Я имею дело с видеофайлом 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? Есть ли у кого-нибудь альтернативный метод кэширования видео?
PS: Вам необходимо указать формат видео или даже расширение файла. Это может дать нам представление о проблеме (например, фраза «URL-адрес моего видео — это ссылка на Youtube» сразу говорит нам, почему это не сработает). Предоставьте проверяемую ссылку на видео, чтобы избежать догадок... Может быть много чего происходит,,, (1) Вы загрузили зашифрованное видео? (2) Ваше видео не является видео (например: может быть, это на самом деле файл списка воспроизведения .m3u8
)? (3) По соображениям безопасности ему не нравится это file:///
на вашем пути, если только вместо этого вы не используете API выбора файлов (где обычно разрешено воспроизведение выбранного пользователем файла).
@VC.One Да, вы правы, на самом деле я только начал подозревать, что это может быть проблемой, и тестирование с предоставленным вами видео помогло это подтвердить. Это связано с тем, что API, который я использую, возвращает видео в формате .m3u8, хотя это только отдельные видео, а не списки воспроизведения. Знаешь ли ты, как мне тогда справиться с этим делом?
Попробуйте проверить файл с помощью MediaInfo Online. Что там говорится о его формате? В любом случае, если это один файл, вы можете попробовать переименовать его. Попробуйте переименовать его в filename.mp4
вместо filename.m3u8
, а затем протестируйте воспроизведение с помощью AVPlayer... Кроме того, файл достаточно большой, чтобы быть видео, верно?
developer.apple.com/documentation/avfoundation/…
@VC.One Извините, что неправильно выразился, это файл m3u8, описывающий список воспроизведения, но это всегда список воспроизведения только из одного видео. В настоящее время я изучаю, как получить это видео и легко преобразовать его в MP4 для локального кэширования, но простое изменение имени файла не работает.
Вы видите (a) #EXT-X-STREAM-INF
или вы видите (b) #EXTINF:
, показывающее в тексте m3u8?
@VC.One Итак, файл, полученный напрямую из API, имеет #EXT-X-STREAM-INF
, а последующая строка связывает другой m3u8
файл. При переходе к этому файлу есть #EXTINF
и строка после него, связывающая файл .ts
.
@fadipon Хорошо, имея эту информацию, я могу написать ответ. По сути, вам необходимо загрузить второй файл m3u8 (с указанием файла TS), а также связанный с ним файл TS. Затем поместите оба файла в подпапку вашего кеша. PS: Я подозреваю, что, поскольку этот единственный файл TS представляет все видео, может быть, достаточно просто попробовать воспроизвести только файл TS с помощью AVPlayer?
@VC.One К сожалению, AVPlayer не может воспроизводить файлы TS, его необходимо сначала закодировать в m3u8. Вот почему я не уверен, достаточно ли кэширования файлов, если только невозможно создать файл m3u8, который кодирует и указывает на файл TS, который хранится локально (или что-то еще, что позволяет AVPlayer читать файл TS).
@fadipon Да, кэширования файлов достаточно. Возможно ли создать файл m3u8, который кодирует..? Да, но я хотел избежать этого и сначала протестировать сам файл TS. PS: Извините за такое сумасшедшее время ответа... Позвольте мне написать вам полный ответ, который может помочь вам лучше.
Чтобы воспроизведение заработало, вы можете попробовать выполнить следующие действия:
(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-потоки и кэшировать их во временном каталоге для последующего извлечения.
Этот 10-секундный пример файла MP4 работает из вашего кеша? С этим файлом...
AVPlayer
загружает версиюhttps://
, но не версиюfile:///
path, или обе работают нормально?