Nano Hash - криптовалюты, майнинг, программирование

Как добавить внешние субтитры WebVTT в HTTP Live Stream на клиенте iOS

У нас есть видео, закодированные через bitmovin.com и предоставляемые как HTTP Live Streams (Fairplay HLS), но субтитры, хотя и в формате WebVTT, отображаются отдельно как прямые URL-адреса для всего файла, а не отдельных сегментов, и не являются частью плейлиста HLS m3u8.

Я ищу способ, как внешний файл .vtt, загруженный отдельно, все еще может быть включен в поток HLS и доступен как субтитр в AVPlayer.

Я знаю, что Apple рекомендует включать сегментированные субтитры VTT в список воспроизведения HLS, но я не могу изменить реализацию сервера прямо сейчас, поэтому я хочу уточнить, возможно ли даже предоставить субтитры AVPlayer для игры вместе с потоком HLS. .

Единственный действительный пост на эту тему, утверждающий, что это возможно, - это: Субтитры для AVPlayer / MPMoviePlayerController. Однако пример кода загружает локальный файл mp4 из пакета, и я изо всех сил пытаюсь заставить его работать для списка воспроизведения m3u8 через AVURLAsset. На самом деле у меня проблема с получением videoTrack из удаленного потока m3u8, поскольку asset.tracks(withMediaType: AVMediaTypeVideo) возвращает пустой массив. Есть идеи, может ли этот подход работать для реального потока HLS? Или есть другой способ воспроизвести отдельные субтитры WebVTT с потоком HLS без включения их в список воспроизведения HLS на сервере? Спасибо.

func playFpsVideo(with asset: AVURLAsset, at context: UIViewController) {

    let composition = AVMutableComposition()

    // Video
    let videoTrack = composition.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: kCMPersistentTrackID_Invalid)

    do {

        let tracks = asset.tracks(withMediaType: AVMediaTypeVideo)

        // ==> The code breaks here, tracks is an empty array
        guard let track = tracks.first else {
            Log.error("Can't get first video track")
            return
        }

        try videoTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, asset.duration), of: track, at: kCMTimeZero)

    } catch {

        Log.error(error)
        return
    }


    // Subtitle, some test from the bundle..
    guard let subsUrl = Bundle.main.url(forResource: "subs", withExtension: "vtt") else {
        Log.error("Can't load subs.vtt from bundle")
        return
    }

    let subtitleAsset = AVURLAsset(url: subsUrl)

    let subtitleTrack = composition.addMutableTrack(withMediaType: AVMediaTypeText, preferredTrackID: kCMPersistentTrackID_Invalid)

    do {

        let subTracks = subtitleAsset.tracks(withMediaType: AVMediaTypeText)

        guard let subTrack = subTracks.first else {
            Log.error("Can't get first subs track")
            return
        }

        try subtitleTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, asset.duration), of: subTrack, at: kCMTimeZero)

    } catch {

        Log.error(error)
        return
    }


    // Prepare item and play it
    let item = AVPlayerItem(asset: composition)

    let player = AVPlayer(playerItem: item)

    let playerViewController = AVPlayerViewController()
    playerViewController.player = player

    self.playerViewController = playerViewController

    context.present(playerViewController, animated: true) {
        playerViewController.player?.play()
    }
}

  • Удачи с этой проблемой? Я столкнулся с той же проблемой. Спасибо. 23.08.2017
  • В этом сценарии невозможно добавить субтитры на устройстве. Я решил это на бэкэнде, изменив плейлисты m3u8, создав действительный плейлист с дорожками субтитров. У меня был действительный разговор с автором ответа на связанный вопрос, но позже он был удален, так как не отвечал на вопрос. Во всяком случае, в разговоре мы пришли к выводу, что это невозможно, поскольку он использовал прямые потоки mp4, а не плейлисты m3u8. 23.08.2017
  • Привет, @MartinKoles, ты нашел способ решить эту проблему? 24.05.2018
  • @allenlinli Как упоминалось выше, невозможно добавить субтитры в поток на клиенте. Это должно быть сделано на бэкэнде, и должен быть предоставлен правильный список воспроизведения с дорожками субтитров. 24.05.2018

Ответы:


1

Я понял это. Это заняло целую вечность, и я ненавидел это. Я помещаю свое объяснение и исходный код на Github, но я также помещу сюда кое-что, на случай, если ссылка умрет по какой-либо причине: https://github.com/kanderson-wellbeats/sideloadWebVttToAVPlayer

Я бросаю это объяснение здесь, чтобы попытаться избавить некоторых будущих людей от боли. Многое из того, что я нашел в Интернете, было неправильным, или упущено из виду запутанные части, или содержало кучу дополнительной нерелевантной информации, или смесь всех трех. Вдобавок я видел, как много людей просят о помощи и пытаются сделать то же самое, но никто не дает четких ответов.

Итак, для начала я опишу, что я пытаюсь сделать. Мой внутренний сервер - это службы мультимедиа Azure, и он действительно отлично подходит для потоковой передачи видео с разным разрешением по мере необходимости, но на самом деле он не поддерживает WebVtt. Да, вы можете разместить там файл, но, похоже, он не может предоставить нам основной список воспроизведения, который включает ссылку на список воспроизведения субтитров (как требует Apple). Кажется, и Apple, и Microsoft решили, что они собираются делать с субтитрами еще в 2012 году, и с тех пор не трогали его. В то время они либо не разговаривали друг с другом, либо намеренно пошли в противоположные стороны, но у них была плохая взаимная совместимость, и теперь такие разработчики, как мы, вынуждены сокращать разрыв между гигантами. Многие онлайн-ресурсы, посвященные этой теме, посвящены таким вещам, как оптимизированное кэширование произвольных потоковых данных, но я обнаружил, что эти ресурсы скорее сбивают с толку, чем полезны. Все, что я хочу сделать, это добавить субтитры к видео по запросу, воспроизводимому в AVPlayer, обслуживаемом Azure Media Services с протоколом HLS, когда у меня есть размещенный файл WebVtt - ни больше, ни меньше. Я начну с описания всего словами, а потом поставлю код в конце.

Вот чрезвычайно сжатая версия того, что вам нужно сделать:

  1. Перехватывать запросы для основного списка воспроизведения и возвращать его отредактированную версию, которая ссылается на списки воспроизведения субтитров (несколько для нескольких языков или только один для одного языка)
  2. Выберите субтитры для отображения (хорошо задокументировано на https://developer.apple.com/documentation/avfoundation/media_playback_and_selection/selecting_subtitles_and_alternative_audio_tracks)
  3. Перехватывать запросы к спискам воспроизведения субтитров, которые будут проходить (после того, как вы выбрали субтитры для отображения) и возвращать списки воспроизведения, которые вы создали на лету, которые ссылаются на файлы WebVtt на сервере

Вот и все. Не слишком много, за исключением того, что мне пришлось столкнуться с множеством сложностей, которые мне пришлось открыть для себя. Я опишу их сначала кратко, а затем более подробно.

Краткие объяснения осложнений:

  1. Многие запросы будут проходить, но вы должны (и можете только) обработать пару из них самостоятельно, остальные должны пройти без изменений. Я опишу, какие из них нужно обрабатывать, а какие нет, и как с ними обращаться.
  2. Apple решила, что простого HTTP-запроса недостаточно, и решила скрыть ситуацию, переведя его в странную вещь с двойной идентификацией AVAssetResourceLoadingRequest, которая имеет свойство DataRequest (AVAssetResourceLoadingDataRequest) и свойство ContentInformationRequest (AVAssetResourceLoadingContentInformationRequest). Я до сих пор не понимаю, почему это было необходимо и какую пользу это приносит, но то, что я сделал с ними, работает. Некоторые многообещающие блоги / ресурсы, кажется, предлагают вам возиться с ContentInformationRequest, но я считаю, что вы можете просто игнорировать ContentInformationRequest, и на самом деле возиться с ним чаще, чем просто ломает вещи.
  3. Apple предлагает вам сегментировать файл VTT на мелкие части, но вы просто не можете сделать это на стороне клиента (Apple запрещает это), но, к счастью, также кажется, что вам на самом деле не нужно этого делать, это просто предложение.

ПЕРЕМЕЩЕНИЕ ЗАПРОСОВ

Чтобы перехватить запросы, вы должны создать подкласс / расширить AVAssetResourceLoaderDelegate, и интересующий метод - это метод ShouldWaitForLoadingOfRequestedResource. Чтобы использовать делегата, создайте экземпляр своего AVPlayer, передав ему AVPlayerItem, но передайте AVPlayerItem AVUrlAsset, у которого есть свойство делегата, которому вы назначаете делегата. Все запросы будут проходить через метод ShouldWaitForLoadingOfRequestedResource, так что именно там будет происходить весь бизнес, за исключением одного скрытого осложнения - метод будет вызываться только в том случае, если запросы начинаются с чего-то другого, кроме http / https, поэтому я советую придерживаться постоянной строки в начале URL-адреса, который вы используете для создания своего AVUrlAsset, который вы можете просто сбрить после того, как запросы поступят к вашему делегату - давайте назовем это CUSTOMSCHEME. Эта часть описана в нескольких местах в Интернете, но это может быть очень неприятно, если вы не знаете, что вам нужно это делать, потому что будет казаться, что вообще ничего не происходит.

ПЕРЕХВАТ - ТИП A) перенаправление

Хорошо, теперь мы перехватываем запросы, но вы не хотите (/ не можете) обрабатывать их все самостоятельно. Некоторые запросы вы просто хотите разрешить. Вы делаете это следующим образом:

  1. создайте новый NSUrlRequest для ИСПРАВЛЕННОГО URL (удалите эту часть CUSTOMSCHEME из ранее) и установите для него свойство Redirect в LoadingRequest
  2. создайте новый NSHttpUrlResponse с тем же исправленным URL-адресом и кодом 302 и установите его в свойстве Response в LoadingRequest
  3. вызвать FinishLoading в запросе LoadingRequest
  4. вернуть истину

С помощью этих шагов вы можете добавить точки останова и прочее для отладки и проверки всех поступающих запросов, но они будут работать нормально, так что вы ничего не сломаете. Однако этот подход нужен не только для отладки, но и для нескольких запросов даже в готовом проекте.

ПЕРЕХВАТ - ТИП B) ответ редактирования / подделки

Когда приходят какие-то запросы, вы захотите сделать собственный запрос, чтобы ответ на ваш запрос (с некоторыми настройками) можно было использовать для выполнения LoadingRequest. Так что сделайте следующее:

  1. создайте NSUrlSession и вызовите метод CreateDataTask в сеансе (с исправленным URL-адресом - удалите CUSTOMSCHEME)
  2. вызвать Resume в DataTask (вне обратного вызова в DataTask)
  3. вернуть истину
  4. в обратном вызове DataTask у вас будут данные, поэтому (после внесения изменений) вы вызываете Respond в свойстве DataRequest LoadingRequest с этими (отредактированными) данными, а затем вызываете FinishLoading в LoadingRequest

ПЕРЕСЕЧЕНИЕ - какие запросы получают, какой тип лечения

Будет поступать множество запросов, некоторые из них необходимо перенаправить, на некоторые нужно дать ответы с изготовленными / измененными данными. Вот типы запросов, которые вы увидите в порядке их поступления, и что делать с каждым:

  1. запрос к главному списку воспроизведения, но RequestedLength DataRequest равен 2 - просто перенаправьте (ТИП A)
  2. запрос к главному плейлисту, но RequestedLength DataRequest соответствует (неотредактированной) длине главного плейлиста - сделайте свой собственный запрос к главному плейлисту, чтобы вы могли его отредактировать и вернуть отредактированный результат (ТИП B)
  3. запрос к мастеру воспроизведения, но RequestedLength DataRequest огромен - сделайте то же, что и для предыдущего (ТИП B)
  4. будет много запросов на фрагменты аудио и видео - все эти запросы нужно перенаправить (ТИП A)
  5. как только вы правильно отредактируете основной список воспроизведения (и выберете субтитры), будет получен запрос для списка воспроизведения субтитров - отредактируйте его, чтобы вернуть созданный список воспроизведения субтитров (ТИП B)

КАК РЕДАКТИРОВАТЬ ПЛЕЙЛИСТЫ - основной плейлист

Главный список воспроизведения легко редактировать. Изменение состоит в двух вещах:

  1. у каждого видеоресурса есть своя строка, и всем нужно рассказать о группе субтитров (для каждой строки, которая начинается с #EXT-X-STREAM-INF, я добавляю ,SUBTITLES="subs" в конце)
  2. необходимо добавить новые строки для каждого языка / типа субтитров, все из которых принадлежат группе субтитров с их собственным URL-адресом (поэтому для каждого типа добавьте строку типа #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",LANGUAGE="!!!yourLanguageHere!!!",NAME="!!!yourNameHere!!!",AUTOSELECT=YES,URI="!!!yourCustomUrlHere!!!"

!!! yourCustomUrlHere !!! вы используете на шаге 2, должны быть обнаружены вами, когда он используется для запроса, чтобы вы могли вернуть созданный список воспроизведения субтитров как часть ответа, поэтому установите для него что-то уникальное. Этот URL-адрес также должен будет использовать параметр CUSTOMSCHEME, чтобы он поступил к делегату. Вы также можете проверить этот пример потоковой передачи, чтобы увидеть, как должен выглядеть манифест: https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html (проанализируйте сетевой трафик с помощью отладчика браузера, чтобы увидеть его).

КАК РЕДАКТИРОВАТЬ ПЛЕЙЛИСТЫ - плейлист с субтитрами

Плейлист с субтитрами немного сложнее. Вы должны сделать все самостоятельно. То, как я это сделал, - это фактически захватить файл WebVtt внутри обратного вызова DataTask, затем проанализировать его, чтобы найти конец самой последней последовательности временных меток, преобразовать это в целое число секунд, а затем вставить это значение в паре мест большой строкой. Опять же, вы можете использовать приведенный выше пример и проанализировать сетевой трафик, чтобы увидеть реальный пример. Вот так это выглядит:

#EXTM3U
#EXT-X-TARGETDURATION:!!!thatLengthIMentioned!!!
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:!!!thatLengthIMentioned!!!
!!!absoluteUrlToTheWebVttFileOnTheServer!!!
#EXT-X-ENDLIST

Обратите внимание, что список воспроизведения НЕ сегментирует файл vtt, как рекомендует Apple, потому что это невозможно сделать на стороне клиента (источник: https://developer.apple.com/forums/thread/113063?answerId=623328022#623328022). Также обратите внимание, что я НЕ ставлю запятую в конце строки EXTINF, хотя пример Apple здесь говорит об этом, потому что кажется, что это нарушает его: https://developer.apple.com/videos/play/wwdc2012/512/

Теперь собственно код:

public class CustomResourceLoaderDelegate : AVAssetResourceLoaderDelegate
{
    public const string LoaderInterceptionWorkaroundUrlPrefix = "CUSTOMSCHEME"; // a scheme other than http(s) needs to be used for AVUrlAsset's URL or ShouldWaitForLoadingOfRequestedResource will never be called
    private const string SubtitlePlaylistBoomerangUrlPrefix = LoaderInterceptionWorkaroundUrlPrefix + "SubtitlePlaylist";
    private const string SubtitleBoomerangUrlSuffix = "m3u8";
    private readonly NSUrlSession _session;
    private readonly List<SubtitleBundle> _subtitleBundles;

    public CustomResourceLoaderDelegate(IEnumerable<WorkoutSubtitleDto> subtitles)
    {
        _subtitleBundles = subtitles.Select(subtitle => new SubtitleBundle {SubtitleDto = subtitle}).ToList();
        _session = NSUrlSession.FromConfiguration(NSUrlSessionConfiguration.DefaultSessionConfiguration);
    }

    public override bool ShouldWaitForLoadingOfRequestedResource(AVAssetResourceLoader resourceLoader,
        AVAssetResourceLoadingRequest loadingRequest)
    {
        var requestString = loadingRequest.Request.Url.AbsoluteString;
        var dataRequest = loadingRequest.DataRequest;

        if (requestString.StartsWith(SubtitlePlaylistBoomerangUrlPrefix))
        {
            var uri = new Uri(requestString);
            var targetLanguage = uri.Host.Split(".").First();
            var targetSubtitle = _subtitleBundles.FirstOrDefault(s => s.SubtitleDto.Language == targetLanguage);

            Debug.WriteLine("### SUBTITLE PLAYLIST " + requestString);
            if (targetSubtitle == null)
            {
                loadingRequest.FinishLoadingWithError(new NSError());
                return true;
            }
            var subtitlePlaylistTask = _session.CreateDataTask(NSUrlRequest.FromUrl(NSUrl.FromString(targetSubtitle.SubtitleDto.CloudFileURL)),
                (data, response, error) =>
                {
                    if (error != null)
                    {
                        loadingRequest.FinishLoadingWithError(error);
                        return;
                    }
                    if (data == null || !data.Any())
                    {
                        loadingRequest.FinishLoadingWithError(new NSError());
                        return;
                    }
                    MakePlaylistAndFragments(targetSubtitle, Encoding.UTF8.GetString(data.ToArray()));

                    loadingRequest.DataRequest.Respond(NSData.FromString(targetSubtitle.Playlist));
                    loadingRequest.FinishLoading();
                });
            subtitlePlaylistTask.Resume();
            return true;
        }

        if (!requestString.ToLower().EndsWith(".ism/manifest(format=m3u8-aapl)") || // lots of fragment requests will come through, we're just going to fix their URL so they can proceed normally (getting bits of video and audio)
            (dataRequest != null && 
             dataRequest.RequestedOffset == 0 && // this catches the first (of 3) master playlist requests. the thing sending out these requests and handling the responses seems unable to be satisfied by our handling of this (just for the first request), so that first request is just let through. if you mess with request 1 the whole thing stops after sending request 2. although this means the first request doesn't get the same edited master playlist as the second or third, apparently that's fine.
             dataRequest.RequestedLength == 2 &&
             dataRequest.CurrentOffset == 0))
        {
            Debug.WriteLine("### REDIRECTING REQUEST " + requestString);
            var redirect = new NSUrlRequest(new NSUrl(requestString.Replace(LoaderInterceptionWorkaroundUrlPrefix, "")));
            loadingRequest.Redirect = redirect;
            var fakeResponse = new NSHttpUrlResponse(redirect.Url, 302, null, null);
            loadingRequest.Response = fakeResponse;
            loadingRequest.FinishLoading();
            return true;
        }

        var correctedRequest = new NSMutableUrlRequest(new NSUrl(requestString.Replace(LoaderInterceptionWorkaroundUrlPrefix, "")));
        if (dataRequest != null)
        {
            var headers = new NSMutableDictionary();
            foreach (var requestHeader in loadingRequest.Request.Headers)
            {
                headers.Add(requestHeader.Key, requestHeader.Value);
            }
            correctedRequest.Headers = headers;
        }

        var masterPlaylistTask = _session.CreateDataTask(correctedRequest, (data, response, error) =>
        {
            Debug.WriteLine("### REQUEST CARRIED OUT AND RESPONSE EDITED " + requestString);
            if (error == null)
            {
                var dataString = Encoding.UTF8.GetString(data.ToArray());
                var stringWithSubsAdded = AddSubs(dataString);

                dataRequest?.Respond(NSData.FromString(stringWithSubsAdded));

                loadingRequest.FinishLoading();
            }
            else
            {
                loadingRequest.FinishLoadingWithError(error);
            }
        });
        masterPlaylistTask.Resume();
        return true;
    }

    private string AddSubs(string dataString)
    {
        var tracks = dataString.Split("\r\n").ToList();
        for (var ii = 0; ii < tracks.Count; ii++)
        {
            if (tracks[ii].StartsWith("#EXT-X-STREAM-INF"))
            {
                tracks[ii] += ",SUBTITLES=\"subs\"";
            }
        }

        tracks.AddRange(_subtitleBundles.Select(subtitle => "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",LANGUAGE=\"" + subtitle.SubtitleDto.Language + "\",NAME=\"" + subtitle.SubtitleDto.Title + "\",AUTOSELECT=YES,URI=\"" + SubtitlePlaylistBoomerangUrlPrefix + "://" + subtitle.SubtitleDto.Language + "." + SubtitleBoomerangUrlSuffix + "\""));

        var finalPlaylist = string.Join("\r\n", tracks);
        return finalPlaylist;
    }

    private void MakePlaylistAndFragments(SubtitleBundle subtitle, string vtt)
    {
        var noWhitespaceVtt = vtt.Replace(" ", "").Replace("\n", "").Replace("\r", "");
        var arrowIndex = noWhitespaceVtt.LastIndexOf("-->");
        var afterArrow = noWhitespaceVtt.Substring(arrowIndex);
        var firstColon = afterArrow.IndexOf(":");
        var period = afterArrow.IndexOf(".");
        var timeString = afterArrow.Substring(firstColon - 2, period /*(+ 2 - 2)*/);
        var lastTime = (int)TimeSpan.Parse(timeString).TotalSeconds;

        var resultLines = new List<string>
        {
            "#EXTM3U",
            "#EXT-X-TARGETDURATION:" + lastTime,
            "#EXT-X-VERSION:3",
            "#EXT-X-MEDIA-SEQUENCE:0",
            "#EXT-X-PLAYLIST-TYPE:VOD",
            "#EXTINF:" + lastTime,
            subtitle.SubtitleDto.CloudFileURL,
            "#EXT-X-ENDLIST"
        };

        subtitle.Playlist = string.Join("\r\n", resultLines);
    }

    private class SubtitleBundle
    {
        public WorkoutSubtitleDto SubtitleDto { get; set; }
        public string Playlist { get; set; }
    }

    public class WorkoutSubtitleDto
    {
        public int WorkoutID { get; set; }
        public string Language { get; set; }
        public string Title { get; set; }
        public string CloudFileURL { get; set; }
    }
}
28.04.2021
Новые материалы

Кластеризация: более глубокий взгляд
Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

Как написать эффективное резюме
Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

Частный метод Python: улучшение инкапсуляции и безопасности
Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

Как я автоматизирую тестирование с помощью Jest
Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

Понимание расстояния Вассерштейна: мощная метрика в машинном обучении
В обширной области машинного обучения часто возникает необходимость сравнивать и измерять различия между распределениями вероятностей. Традиционные метрики расстояния, такие как евклидово..

Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..