Mar*_*les 5 avfoundation http-live-streaming ios avplayer avkit
我们有通过bitmovin.com编码的视频,并以HTTP Live Streams(Fairplay HLS)的形式提供,但字幕虽然是WebVTT格式,但是作为整个文件的直接URL单独公开,而不是单个段,并且不是HLS m3u8播放列表的一部分.
我正在寻找单独下载的外部.vtt文件如何仍然包含在HLS流中并作为AVPlayer中的副标题提供的方式.
我知道Apple的建议是将分段的VTT字幕包含在HLS播放列表中,但我现在无法改变服务器实现,所以我想澄清是否有可能为AVPlayer提供字幕以与HLS流一起播放.
关于这个主题的唯一有效帖子声称它是可能的:AVPlayer/MPMoviePlayerController的字幕.但是,示例代码从bundle加载本地mp4文件,我正在努力使其适用于m3u8播放列表AVURLAsset.实际上,我有问题从远程m3u8流中获取videoTrack作为asset.tracks(withMediaType: AVMediaTypeVideo)返回空数组.任何想法,如果这种方法可以用于真正的HLS流?或者有没有其他方法可以使用HLS流播放单独的WebVTT字幕,而不将它们包含在服务器上的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()
}
}
Run Code Online (Sandbox Code Playgroud)
我明白了这一点。这花了很长时间,我讨厌它。我将我的解释和源代码放在 Github 上,但我也会将内容放在这里,以防链接因某种原因失效: https: //github.com/kanderson-wellbeats/sideloadWebVttToAVPlayer
我在这里放弃这个解释是为了避免一些未来的人遭受很多痛苦。我在网上发现的很多东西都是错误的,或者遗漏了令人困惑的部分,或者有一堆额外的不相关信息,或者三者兼而有之。最重要的是,我看到很多人寻求帮助并试图做同样的事情,但没有人提供任何明确的答案。
首先,我将描述我正在尝试做的事情。我的后端服务器是 Azure 媒体服务,它非常适合根据需要流式传输不同分辨率的视频,但它并不真正支持 WebVtt。是的,你可以在那里托管一个文件,但它似乎无法为我们提供一个包含字幕播放列表引用的主播放列表(按照苹果的要求)。苹果和微软似乎早在 2012 年就决定了如何处理字幕,此后就没有再碰过它。当时他们要么互不交谈,要么故意走向相反的方向,但恰好互兼容性很差,现在像我们这样的开发者被迫拉大与庞然大物之间的差距。许多涵盖该主题的在线资源都在解决诸如任意流数据的优化缓存之类的问题,但我发现这些资源更令人困惑而不是有帮助。我想要做的就是当我有托管的 WebVtt 文件时,为 Azure 媒体服务使用 HLS 协议提供的 AVPlayer 中播放的点播视频添加字幕 - 仅此而已。我将首先用文字描述所有内容,然后将实际代码放在最后。
就是这样。没有太多,除了有很多复杂的事情阻碍我去发现自己。我将首先简要描述它们,然后再更详细地描述它们。
要拦截请求,您必须子类/扩展 AVAssetResourceLoaderDelegate 并且感兴趣的方法是 ShouldWaitForLoadingOfRequestedResource 方法。要使用委托,请通过向 AVPlayerItem 传递一个 AVPlayerItem 来实例化您的 AVPlayer,但向 AVPlayerItem 传递一个 AVUrlAsset,该 AVUrlAsset 具有您为其分配委托的委托属性。所有请求都将通过 ShouldWaitForLoadingOfRequestedResource 方法进行,因此这就是所有业务发生的地方,除了一个偷偷摸摸的复杂情况 - 仅当请求以 http/https 以外的内容开头时才会调用该方法,因此我的建议是坚持一个常量字符串在您用来创建 AVUrlAsset 的 Url 前面,您可以在请求进入您的委托后将其删除 - 让我们称之为“CUSTOMSCHEME”。这部分在网上的几个地方都有描述,但如果您不知道必须这样做,那可能会非常令人沮丧,因为看起来好像什么也没发生。
好的,现在我们正在拦截请求,但您不想(/不能)自己处理所有请求。您只想允许某些请求通过。您可以通过执行以下操作来完成此操作:
通过这些步骤,您可以添加断点和内容来调试和检查将通过的所有请求,但它们会正常进行,因此您不会破坏任何内容。然而,这种方法不仅仅用于调试,即使在已完成的项目中,对于多个请求也是必要的。
当收到一些请求时,您需要执行自己的请求,以便对您的请求的响应(经过一些调整)可用于满足 LoadingRequest。所以请执行以下操作:
将会收到大量请求,有些需要重定向,有些需要提供制造/更改的数据响应。以下是您将看到的请求类型(按其进入顺序排列)以及如何处理每种请求:
主播放列表很容易编辑。变化有两点:
#EXT-X-STREAM-INF我,SUBTITLES="subs"在末尾添加的开头的每行)#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",LANGUAGE="!!!yourLanguageHere!!!",NAME="!!!yourNameHere!!!",AUTOSELECT=YES,URI="!!!yourCustomUrlHere!!!"您的自定义网址在这里!!!您在步骤 2 中使用的内容在用于请求时必须被您检测到,以便您可以将制造的字幕播放列表作为响应的一部分返回,因此将其设置为唯一的内容。该 Url 还必须使用“CUSTOMSCHEME”事物,以便它到达委托。您还可以查看此流式传输示例以查看清单的外观:https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html(使用浏览器调试器嗅探网络流量以查看它)。
字幕播放列表有点复杂。整个事情你必须自己做。我这样做的方法是在 DataTask 回调中自己实际抓取 WebVtt 文件,然后解析该文件以找到最后一个时间戳序列的末尾,将其转换为整数秒数,然后插入该值在一个大字符串的几个地方。同样,您可以使用上面列出的示例并嗅探网络流量来亲自查看真实的示例。所以它看起来像这样:
#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
Run Code Online (Sandbox Code Playgroud)
请注意,播放列表不会按照 Apple 的建议对 vtt 文件进行分段,因为这无法在客户端完成(来源:https: //developer.apple.com/forums/thread/113063 ?answerId=623328022 #623328022)。另请注意,我没有在“EXTINF”行的末尾添加逗号,即使苹果的示例在这里说要这样做,因为它似乎破坏了它: 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; }
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1294 次 |
| 最近记录: |