Я имею дело с видеофайлом M3U8, который пытаюсь воспроизвести после сохранения таких удаленных видео в моем приложении SwiftUI.
Основной подход, который я нашел на данный момент, — это принятый ответ в этой теме: Возможно ли кэшировать видео? IOS – Свифт
Проблема:
Однако когда я пытаюсь это реализовать, видео M3U8 не загружается.
Если бы я играл прямо из videoURL
, то проблем не было бы и видео воспроизводилось. Но еще хотелось бы поиграть в нее из кеша.
VideoPlayer(player: player)
.onAppear {
CacheManager.shared.getFileWith(stringUrl: videoURL) { result in
switch result {
case .success(let url):
self.player = AVPlayer(url: url)
self.player?.play()
case .failure:
print("Error")
}
}
}
Для контекста печать URL-адреса из CacheManager дает file:///var/mobile/Containers/Data/Application/2F12CCE9-1D0C-447B-9B96-9EC6F6BE1413/Library/Caches/filename
(где имя файла — это фактическое имя файла).
А вот так выглядит видеоплеер при запуске приложения.
Так есть ли проблема с моей реализацией кода? Действительно ли принятый ответ из старой темы недействителен/устарел и больше не работает с текущими версиями iOS? Есть ли у кого-нибудь альтернативный метод кэширования видео?
Чтобы воспроизведение заработало, вы можете попробовать выполнить следующие действия:
(1) Загрузите .m3u8
и связанные с ним .ts
файлы, указанные в файле M3U8.
(2) Внутри M3U8 измените текст пути, удалив все подпапки.
например: если ваш путь M3U8: /cdn_url/video720/filename.ts
просто удалите текст /cdn_url/video720/
.
Тогда ваш M3U8 должен выглядеть примерно так (где меняется только собственный путь к файлу):
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:30
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:30.0,
filename.ts
#EXT-X-ENDLIST
Также рекомендуется переименовать M3U8 и TS, указав соответствующее «имя» файла для идентификации видео в списке воспроизведения.
например: Сделайте это как Batman_ep_01.m3u8
и Batman_ep_01.ts
(3) Сохраните и протестируйте в AVPlayer.
А как насчет нескольких файлов TS в одном M3U8?
Он работает так же, как шаг (2), но вы просто помещаете M3U8 и связанные с ним файлы TS в уникальную подпапку. Что-то вроде ниже, где хранятся файлы m3u8 и ts.
например: /Library/Caches/some_Show_multi/
В итоге я в основном реализовал этот подход: https://developer.apple.com/documentation/avfoundation/offline_playback_and_storage/using_avfoundation_to_play_and_persist_http_live_streams.
В примере кода продемонстрирован метод загрузки содержимого HLS, однако видео сохранялись в общей папке, к которой пользователи могли получить доступ из настроек, а также использовали UserDefaults для хранения местоположений, обе функции мне не нужны, поскольку я хотел реализовать кеш, который загружает видео при их загрузке, чтобы удалить их позже.
Поэтому я изменил и упростил код следующим образом:
class Asset {
var urlAsset: AVURLAsset
var name: String
init(urlAsset: AVURLAsset, name: String) {
self.urlAsset = urlAsset
self.name = name
}
}
class AssetPersistenceManager: NSObject {
static let shared = AssetPersistenceManager()
private var assetDownloadURLSession: AVAssetDownloadURLSession!
private var activeDownloadsMap = [AVAssetDownloadTask: Asset]()
private var willDownloadToUrlMap = [AVAssetDownloadTask: URL]()
private let fileManager = FileManager.default
override private init() {
super.init()
// Create the configuration for the AVAssetDownloadURLSession.
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "AAPL-Identifier")
// Create the AVAssetDownloadURLSession using the configuration.
assetDownloadURLSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration,
assetDownloadDelegate: self,
delegateQueue: OperationQueue.main)
}
func downloadStream(for asset: Asset) async {
guard let task = assetDownloadURLSession.makeAssetDownloadTask(
asset: asset.urlAsset,
assetTitle: asset.urlAsset.url.lastPathComponent,
assetArtworkData: nil,
options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 265_000]
) else { return }
activeDownloadsMap[task] = asset
task.resume()
}
func localAssetForStream(withName name: String) -> AVURLAsset? {
let documentsUrl = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let localFileLocation = documentsUrl.appendingPathComponent(name)
guard !fileManager.fileExists(atPath: localFileLocation.path) else {
return AVURLAsset(url: localFileLocation)
}
return nil
}
func cleanCache() {
do {
let documentsUrl = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let contents = try fileManager.contentsOfDirectory(at: documentsUrl, includingPropertiesForKeys: nil)
for file in contents {
do {
try fileManager.removeItem(at: file)
}
catch {
print("An error occured trying to delete the contents on disk for: \(file).")
}
}
} catch {
print("Failed to clean cache.")
}
}
}
extension AssetPersistenceManager: AVAssetDownloadDelegate {
/// Tells the delegate that the task finished transferring data.
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let task = task as? AVAssetDownloadTask,
let asset = activeDownloadsMap.removeValue(forKey: task) else { return }
guard let downloadURL = willDownloadToUrlMap.removeValue(forKey: task) else { return }
if let error = error as NSError? {
switch (error.domain, error.code) {
case (NSURLErrorDomain, NSURLErrorCancelled):
/*
This task was canceled, you should perform cleanup using the
URL saved from AVAssetDownloadDelegate.urlSession(_:assetDownloadTask:didFinishDownloadingTo:).
*/
guard let localFileLocation = localAssetForStream(withName: asset.name)?.url else { return }
do {
try fileManager.removeItem(at: localFileLocation)
} catch {
print("An error occured trying to delete the contents on disk for \(asset.name): \(error)")
}
case (NSURLErrorDomain, NSURLErrorUnknown):
fatalError("Downloading HLS streams is not supported in the simulator.")
default:
fatalError("An unexpected error occured \(error.domain)")
}
} else {
do {
let documentsUrl = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let newURL = documentsUrl.appendingPathComponent(asset.name)
try fileManager.moveItem(at: downloadURL, to: newURL)
} catch {
print("Failed to move downloaded file to temp directory.")
}
}
}
/// Method called when the an aggregate download task determines the location this asset will be downloaded to.
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
willDownloadToUrlMap[assetDownloadTask] = location
}
}
В частности, я изменил делегата для обработки завершения загрузки, чтобы впоследствии переместить файл в каталог /tmp
, чтобы он больше не отображался в настройках. Теперь это позволяет мне асинхронно загружать HTTP-потоки и кэшировать их во временном каталоге для последующего извлечения.