Как воспроизвести видео M3U8 из кеша в iOS?

Я имею дело с видеофайлом 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 (где имя файла — это фактическое имя файла).

А вот так выглядит видеоплеер при запуске приложения.

Как воспроизвести видео M3U8 из кеша в iOS?

Так есть ли проблема с моей реализацией кода? Действительно ли принятый ответ из старой темы недействителен/устарел и больше не работает с текущими версиями iOS? Есть ли у кого-нибудь альтернативный метод кэширования видео?

Этот 10-секундный пример файла MP4 работает из вашего кеша? С этим файлом... AVPlayer загружает версию https://, но не версию file:/// path, или обе работают нормально?

VC.One 08.07.2024 20:52

PS: Вам необходимо указать формат видео или даже расширение файла. Это может дать нам представление о проблеме (например, фраза «URL-адрес моего видео — это ссылка на Youtube» сразу говорит нам, почему это не сработает). Предоставьте проверяемую ссылку на видео, чтобы избежать догадок... Может быть много чего происходит,,, (1) Вы загрузили зашифрованное видео? (2) Ваше видео не является видео (например: может быть, это на самом деле файл списка воспроизведения .m3u8)? (3) По соображениям безопасности ему не нравится это file:/// на вашем пути, если только вместо этого вы не используете API выбора файлов (где обычно разрешено воспроизведение выбранного пользователем файла).

VC.One 08.07.2024 20:55

@VC.One Да, вы правы, на самом деле я только начал подозревать, что это может быть проблемой, и тестирование с предоставленным вами видео помогло это подтвердить. Это связано с тем, что API, который я использую, возвращает видео в формате .m3u8, хотя это только отдельные видео, а не списки воспроизведения. Знаешь ли ты, как мне тогда справиться с этим делом?

fadipon 08.07.2024 21:02

Попробуйте проверить файл с помощью MediaInfo Online. Что там говорится о его формате? В любом случае, если это один файл, вы можете попробовать переименовать его. Попробуйте переименовать его в filename.mp4 вместо filename.m3u8, а затем протестируйте воспроизведение с помощью AVPlayer... Кроме того, файл достаточно большой, чтобы быть видео, верно?

VC.One 08.07.2024 21:18

developer.apple.com/documentation/avfoundation/…

lorem ipsum 08.07.2024 22:09

@VC.One Извините, что неправильно выразился, это файл m3u8, описывающий список воспроизведения, но это всегда список воспроизведения только из одного видео. В настоящее время я изучаю, как получить это видео и легко преобразовать его в MP4 для локального кэширования, но простое изменение имени файла не работает.

fadipon 09.07.2024 15:16

Вы видите (a) #EXT-X-STREAM-INF или вы видите (b) #EXTINF:, показывающее в тексте m3u8?

VC.One 09.07.2024 19:45

@VC.One Итак, файл, полученный напрямую из API, имеет #EXT-X-STREAM-INF, а последующая строка связывает другой m3u8 файл. При переходе к этому файлу есть #EXTINF и строка после него, связывающая файл .ts.

fadipon 09.07.2024 20:55

@fadipon Хорошо, имея эту информацию, я могу написать ответ. По сути, вам необходимо загрузить второй файл m3u8 (с указанием файла TS), а также связанный с ним файл TS. Затем поместите оба файла в подпапку вашего кеша. PS: Я подозреваю, что, поскольку этот единственный файл TS представляет все видео, может быть, достаточно просто попробовать воспроизвести только файл TS с помощью AVPlayer?

VC.One 09.07.2024 22:12

@VC.One К сожалению, AVPlayer не может воспроизводить файлы TS, его необходимо сначала закодировать в m3u8. Вот почему я не уверен, достаточно ли кэширования файлов, если только невозможно создать файл m3u8, который кодирует и указывает на файл TS, который хранится локально (или что-то еще, что позволяет AVPlayer читать файл TS).

fadipon 09.07.2024 22:28

@fadipon Да, кэширования файлов достаточно. Возможно ли создать файл m3u8, который кодирует..? Да, но я хотел избежать этого и сначала протестировать сам файл TS. PS: Извините за такое сумасшедшее время ответа... Позвольте мне написать вам полный ответ, который может помочь вам лучше.

VC.One 11.07.2024 02:28
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
11
164
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Чтобы воспроизведение заработало, вы можете попробовать выполнить следующие действия:

(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/

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

fadipon 12.07.2024 15:50
Ответ принят как подходящий

В итоге я в основном реализовал этот подход: 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-потоки и кэшировать их во временном каталоге для последующего извлечения.

Другие вопросы по теме