I am using a modified version of kanderson-wellbeats' delegate to modify requests as AVKit needs them. All code will be at the bottom of the post. All is well in modifying the requests, making the subtitle playlists, and adding them to the master playlist as they do show up when requesting AVMediaCharacteristic.legible
, but as soon as I add the line that adds ",SUBTITLES="subs"" to the end of the "#EXT-X-STREAM-INF:" lines, which should add the subtitles to that video track, it won't play anymore. Removing that part makes it possible to select the subtitle track on the playerItem, but the subtitles do not display on the screen.
我正在使用修改后的kanderson-well beats委托版本来修改AVKit需要的请求。所有代码将在帖子的底部。修改请求、创建字幕播放列表并将它们添加到主播放列表中是很好的,因为它们在请求AVMediaCharacteristic时会显示。但是,只要我在“#ext-X-stream-INF:”行的末尾添加“,Subtiles=”subtiles=“subs”行,它就不会再播放了,这应该会将字幕添加到该视频轨道。移除该部分使得在playerItem上选择字幕轨道成为可能,但是字幕不会显示在屏幕上。
Note that I am using a custom AVPlayer with AVPlayerLayer instead of AVPlayerViewController as I required a lot more customization; maybe that is the issue?
请注意,我使用的是带有AVPlayerLayer的定制AVPlayer,而不是AVPlayerViewController,因为我需要更多的定制;也许这就是问题所在?
I know I could probably just add in an overlay with a UILabel for the subtitles, but I'd rather the solution be more native.
我知道我可能只需要为字幕添加一个带有UILabel的覆盖层,但我更希望解决方案更本地化。
InterceptingAssetResourceLoaderDelegate.swift
Note: in the subtitle playlist here, there is no comma at the end of the "#EXTINF:(rounded)" line. There seems to be no change in adding it or not, but kanderson-wellbeats said that adding it would cause issues, so it has been removed for now.
备注:这里的字幕播放列表中,“#EXTINF:(四舍五入)”行的末尾没有逗号。添加或不添加似乎没有什么变化,但Kanderson-Well Beats表示,添加它会引发问题,因此目前已将其删除。
import AVKit
class InterceptingAssetResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate {
private class SubtitleBundle {
internal init(subtitleDTO: InterceptingAssetResourceLoaderDelegate.SubtitleDTO, playlist: String? = nil) {
self.subtitleDTO = subtitleDTO
self.playlist = playlist
}
let subtitleDTO: SubtitleDTO
var playlist: String?
}
private struct SubtitleDTO {
let language: String
let title: String
let url: String
}
static let videoUrlPrefix = "INTERCEPTEDVIDEO"
static let subtitleUrlPrefix = "INTERCEPTEDSUBTITLE"
static let subtitleUrlSuffix = "m3u8"
private let session: URLSession
private let subtitleBundles: [SubtitleBundle]
init(_ subtitles: [VideoSourceEpisodeUrlSubtitle]) {
self.session = URLSession(configuration: .default)
self.subtitleBundles = subtitles.map({
SubtitleBundle(subtitleDTO: SubtitleDTO(language: $0.language, title: $0.name, url: $0.url))
})
}
func resourceLoader(
_ resourceLoader: AVAssetResourceLoader,
shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest
) -> Bool {
guard let url = loadingRequest.request.url,
let dataRequest = loadingRequest.dataRequest else { return true }
if url.absoluteString.starts(with: Self.subtitleUrlPrefix) {
guard let targetLanguage = url.host?.split(separator: ".").first,
let targetSubtitle = self.subtitleBundles.first(where: { $0.subtitleDTO.language == targetLanguage }),
let subtitleUrl = URL(string: targetSubtitle.subtitleDTO.url) else {
loadingRequest.finishLoading(with: AVError(.unknown))
return true
}
let subtitlePlaylistTask = self.session.dataTask(with: subtitleUrl) { [weak self] data, _, error in
if let error {
loadingRequest.finishLoading(with: error)
return
}
guard let data, !data.isEmpty, let dataString = String(data: data, encoding: .utf8) else {
loadingRequest.finishLoading(with: AVError(.unknown))
return
}
self?.makePlaylistAndFragments(bundle: targetSubtitle, subtitle: dataString)
guard let playlistData = targetSubtitle.playlist?.data(using: .utf8) else {
loadingRequest.finishLoading(with: AVError(.unknown))
return
}
dataRequest.respond(with: playlistData)
}
subtitlePlaylistTask.resume()
return true
}
guard let newUrl = URL(string: url.absoluteString.replacingOccurrences(of: Self.videoUrlPrefix, with: "")) else { return true }
if !(
url.absoluteString.lowercased().hasSuffix(".ism/manifest(format=m3u8-aapl)") ||
url.absoluteString.lowercased().hasSuffix(".m3u8")
) || (
dataRequest.requestedOffset == 0 && dataRequest.requestedLength == 2 && dataRequest.currentOffset == 0
) {
let newRequest = URLRequest(url: newUrl)
loadingRequest.redirect = newRequest
let fakeResponse = HTTPURLResponse(url: newUrl, statusCode: 302, httpVersion: nil, headerFields: nil)
loadingRequest.response = fakeResponse
loadingRequest.finishLoading()
return true
}
var correctedRequest = URLRequest(url: newUrl)
for header in loadingRequest.request.allHTTPHeaderFields ?? [:] {
correctedRequest.addValue(header.value, forHTTPHeaderField: header.key)
}
let masterPlaylistTask = self.session.dataTask(with: correctedRequest) { [weak self] data, _, error in
if let error {
loadingRequest.finishLoading(with: error)
return
}
guard let data,
let dataString = String(data: data, encoding: .utf8),
let withSubs = self?.addSubs(to: dataString),
let withSubsData = withSubs.data(using: .utf8) else {
loadingRequest.finishLoading(with: AVError(.unknown))
return
}
dataRequest.respond(with: withSubsData)
loadingRequest.finishLoading()
}
masterPlaylistTask.resume()
return true
}
func addSubs(to dataString: String) -> String {
guard dataString.contains("#EXT-X-STREAM-INF:") else { return dataString }
var tracks = dataString.split(separator: "\n").map({ $0.hasPrefix("#EXT-X-STREAM-INF:") ? $0 + ",SUBTITLES=\"subs\"" : $0 })
tracks.insert(contentsOf: subtitleBundles.map({
"#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",LANGUAGE=\"\($0.subtitleDTO.language)\",NAME=\"\($0.subtitleDTO.title)\","
+ "AUTOSELECT=YES,URI=\"\(Self.subtitleUrlPrefix)://\($0.subtitleDTO.language).\(Self.subtitleUrlSuffix)\""
}), at: tracks.firstIndex(where: { $0.contains("#EXT-X-STREAM-INF") }) ?? tracks.endIndex)
return tracks.joined(separator: "\n")
}
private func makePlaylistAndFragments(bundle: SubtitleBundle, subtitle: String) {
if let regex = try? NSRegularExpression(pattern: #"(\d{2}:\d{2}:\d{2}.\d{3})"#),
let timeString = regex.matches(in: subtitle, range: NSRange(location: 0, length: subtitle.count)).last.flatMap({
(subtitle as NSString).substring(with: $0.range)
}),
let hour = timeString.split(separator: ":")[safe: 0].flatMap({ Double($0) }), hour.isFinite,
let minute = timeString.split(separator: ":")[safe: 1].flatMap({ Double($0) }), minute.isFinite,
let second = timeString.split(separator: ":")[safe: 2].flatMap({ Double($0) }), second.isFinite {
let rounded = Int(ceil(hour * 3600 + minute * 60 + second))
bundle.playlist = [
"#EXTM3U",
"#EXT-X-TARGETDURATION:\(rounded)",
"#EXT-X-VERSION:3",
"#EXT-X-MEDIA-SEQUENCE:0",
"#EXT-X-PLAYLIST-TYPE:VOD",
"#EXTINF:\(rounded)",
bundle.subtitleDTO.url,
"#EXT-X-ENDLIST"
].joined(separator: "\n")
} else if let regex = try? NSRegularExpression(pattern: #"(\d{2}:\d{2}.\d{3})"#),
let timeString = regex.matches(in: subtitle, range: NSRange(location: 0, length: subtitle.count)).last.flatMap({
(subtitle as NSString).substring(with: $0.range)
}),
let minute = timeString.split(separator: ":")[safe: 0].flatMap({ Double($0) }), minute.isFinite,
let second = timeString.split(separator: ":")[safe: 1].flatMap({ Double($0) }), second.isFinite {
let rounded = Int(ceil(minute * 60 + second))
bundle.playlist = [
"#EXTM3U",
"#EXT-X-TARGETDURATION:\(rounded)",
"#EXT-X-VERSION:3",
"#EXT-X-MEDIA-SEQUENCE:0",
"#EXT-X-PLAYLIST-TYPE:VOD",
"#EXTINF:\(rounded)",
bundle.subtitleDTO.url,
"#EXT-X-ENDLIST"
].joined(separator: "\n")
}
}
}
VideoPlayerViewController.swift
VideoPlayerViewController.swift
func setSubtitles(to subtitles: [VideoSourceEpisodeUrlSubtitle]) {
Task {
if let group = try? await self.playerLayer.player?.currentItem?.asset.loadMediaSelectionGroup(
for: AVMediaCharacteristic.legible
) {
self.captionsButton.menu = UIMenu(children: [
UIAction(
title: "None",
image: self.currentSubtitleTrack == nil ? UIImage(systemName: "checkmark") : nil
) { [weak self] _ in
self?.playerLayer.player?.currentItem?.select(nil, in: group)
self?.currentSubtitleTrack = nil
self?.setSubtitles(to: subtitles)
}
] + subtitles.map({ subtitle in
UIAction(
title: subtitle.name,
image: self.currentSubtitleTrack?.url == subtitle.url ? UIImage(systemName: "checkmark") : nil
) { [weak self] _ in
let options = AVMediaSelectionGroup.mediaSelectionOptions(from: group.options, with: Locale(
identifier: subtitle.language
))
if let option = options.first {
self?.playerLayer.player?.currentItem?.select(option, in: group)
self?.currentSubtitleTrack = subtitle
self?.setSubtitles(to: subtitles)
}
}
}))
}
}
}
VideoSourceEpisodeUrlSubtitle.swift
VideoSourceEpisodeUrlSubtitle.swift
struct VideoSourceEpisodeUrlSubtitle: Codable {
let name: String
let url: String
let language: String
}
Thanks everyone in advance for the help.
提前感谢大家的帮助。
更多回答
我是一名优秀的程序员,十分优秀!