AVPlayer 播放加密m3u8(swift版)

抽了點(diǎn)時(shí)間,把AVPlayer播放加密m3u8鏈接demo寫(xiě)了出來(lái),直接復(fù)制黏貼

M3u8ResourceLoader.swift代碼


/// 蘋(píng)果網(wǎng)站上的一段m3u8鏈接數(shù)據(jù),只是為了展示
let apple_m3u8 = "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT\n#EXT-X-TARGETDURATION:10\n#EXT-X-VERSION:3\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:10, no desc\n#EXT-X-KEY:METHOD=AES-128,URI=\"ckey://devimages.apple.com/samplecode/AVARLDelegateDemo/BipBop_gear3_segmented/crypt0.key\", IV=0x3ff5be47e1cdbaec0a81051bcc894d63\nrdtp://devimages.apple.com/samplecode/AVARLDelegateDemo/BipBop_gear3_segmented/fileSequence0.ts\n#EXTINF:10, no desc\nrdtp://devimages.apple.com/samplecode/AVARLDelegateDemo/BipBop_gear3_segmented/fileSequence1.ts\n#EXTINF:10, no desc\nrdtp://devimages.apple.com/samplecode/AVARLDelegateDemo/BipBop_gear3_segmented/fileSequence2.ts\n#EXTINF:10, no desc\nrdtp://devimages.apple.com/samplecode/AVARLDelegateDemo/BipBop_gear3_segmented/fileSequence3.ts\n#EXTINF:10, no desc\nrdtp://devimages.apple.com/samplecode/AVARLDelegateDemo/BipBop_gear3_segmented/fileSequence4.ts\n#EXT-X-ENDLIST"

class M3u8ResourceLoader: NSObject, AVAssetResourceLoaderDelegate {

    /// 假的鏈接(亂寫(xiě)的,前綴反正不要http或者h(yuǎn)ttps,后綴一定要.m3u8,中間隨便)
    fileprivate let m3u8_url_vir = "m3u8Scheme://abcd.m3u8"
    
    /// 真的鏈接
    fileprivate var m3u8_url: String = ""
    
    /// 單例
    fileprivate static let instance = M3u8ResourceLoader()
    
    /// 獲取單例
    public static var shared: M3u8ResourceLoader {
        get {
            return instance
        }
    }
    
    /// 攔截代理方法
    /// true代表意思:系統(tǒng),你要等等,不能播放,需要等我通知,你才能繼續(xù)(相當(dāng)于系統(tǒng)進(jìn)程被阻斷,直到收到了某些消息,才能繼續(xù)運(yùn)行)
    /// false代表意思:系統(tǒng),你不要等,直接播放
    func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
        
        /// 獲取到攔截的鏈接url
        guard let url = loadingRequest.request.url?.absoluteString else {
            return false
        }
        
        /// 判斷url請(qǐng)求是不是 ts (請(qǐng)求很頻繁,因?yàn)橐粋€(gè)視頻分割成多個(gè)ts,直接放最前)
        if url.hasSuffix(".ts") {
            
            /// 處理的操作異步進(jìn)行
            DispatchQueue.main.async {
                
                /// 在這里可以對(duì)ts鏈接進(jìn)行各種處理,反正都是字符串,處理完畢后更換掉系統(tǒng)原先的請(qǐng)求,用新的url去重新請(qǐng)求
                let newUrl = url.replacingOccurrences(of: "rdtp", with: "http")
                
                if let url = URL(string: newUrl) {

                     /// 發(fā)起新的網(wǎng)絡(luò)請(qǐng)求
                    loadingRequest.redirect = URLRequest(url: url) 
                    loadingRequest.response = HTTPURLResponse(url: url, statusCode: 302, httpVersion: nil, headerFields: nil)
                    
                    /// 如果需要對(duì)ts的數(shù)據(jù)進(jìn)行操作
                    if let data = try? Data(contentsOf: url) {
                          
                        /// 將操作后的數(shù)據(jù)塞給系統(tǒng)
                        loadingRequest.dataRequest?.respond(with: data)
                    
                        /// 通知系統(tǒng)請(qǐng)求結(jié)束
                        loadingRequest.finishLoading()
                    
                    } else {
                    
                        /// 通知系統(tǒng)請(qǐng)求結(jié)束,請(qǐng)求有誤
                        self.finishLoadingError(loadingRequest)
                    }

                  //  /// 通知系統(tǒng)請(qǐng)求結(jié)束
                  //  loadingRequest.finishLoading()
                    
                } else {
                    
                    /// 通知系統(tǒng)請(qǐng)求結(jié)束,請(qǐng)求有誤
                    self.finishLoadingError(loadingRequest)
                }
            }
            
            /// 通知系統(tǒng)等待
            return true
        }
        
        /// 判斷url請(qǐng)求是不是 m3u8 (第一次發(fā)起的是m3u8請(qǐng)求,但是只請(qǐng)求一次,放中間)
        if url == m3u8_url_vir {
            
            /// 處理的操作異步進(jìn)行
            DispatchQueue.global().async {
                
                /// 在這里通過(guò)請(qǐng)求m3u8_url鏈接獲取m3u8的數(shù)據(jù),其實(shí)就是一段字符串(和上面的apple_m3u8字符串相似),將字符串直接轉(zhuǎn)為Data格式,可以直接從網(wǎng)上下載,直接轉(zhuǎn)為Data,有一點(diǎn)必須注意,網(wǎng)絡(luò)請(qǐng)求必須是同步的,不能為異步的
                
                if let data = self.M3u8Request(self.m3u8_url) {
                    DispatchQueue.main.async {
                        
                        /// 獲取到原始m3u8字符串
                        if let m3u8String = String(data: data, encoding: .utf8) {
                            
                            /// 可以對(duì)字符串進(jìn)行任意的修改,比如:
                            /// 1、后端對(duì)URI里面的鏈接進(jìn)行過(guò)加密,可以在這里解密后修改替換回去
                            /// 2、URI鏈接沒(méi)進(jìn)行前綴替換,前綴還是http或者h(yuǎn)ttps的,系統(tǒng)請(qǐng)求之后是不會(huì)繼續(xù)執(zhí)行代理方法里面攔截之后的任何操作,這需要我們手動(dòng)替換前綴,上面的字符串前綴是替換過(guò)的(還不明白的自己看上面URI里面的鏈接)
                            /// 3、后端對(duì)ts鏈接進(jìn)行過(guò)加密,同1,
                            
                            /// 當(dāng)然不止這3種操作,還有很多,只要你能想到,但是這些修改操作后,都必須要保證修改后的字符串,進(jìn)行格式化后,還是m3u8格式的字符串
                            
                            /// 還原m3u8字符串
                            let newM3u8String = m3u8String.replacingOccurrences(of: "替換字符串", with: "BipBop")
                            
                            /// 將字符串轉(zhuǎn)化為數(shù)據(jù)
                            let data = newM3u8String.data(using: .utf8)!
                            
                            /// 將數(shù)據(jù)塞給系統(tǒng)
                            loadingRequest.dataRequest?.respond(with: data)
                            
                            /// 通知系統(tǒng)請(qǐng)求結(jié)束
                            loadingRequest.finishLoading()
                        }
                    }
                } else {
                    
                    DispatchQueue.main.async {
                        
                        /// 通知系統(tǒng)請(qǐng)求結(jié)束,請(qǐng)求有誤
                        self.finishLoadingError(loadingRequest)
                    }
                }
            }
            
            /// 通知系統(tǒng)等待
            return true
        }
        
        /// 判斷url請(qǐng)求是不是 key (key只請(qǐng)求一次,就放最后面)
        if !url.hasSuffix(".ts") && url != m3u8_url_vir {
            
            /// 處理的操作異步進(jìn)行
            DispatchQueue.main.async {
                
                /// 獲取key的數(shù)據(jù),其實(shí)也是一串字符串,如果需要驗(yàn)證證書(shū)之類的,用Alamofire請(qǐng)求吧,同上面的m3u8一樣,也要同步
                
                /// 在這里對(duì)字符串進(jìn)行任意修改,解密之類的,同上
                let newUrl = url.replacingOccurrences(of: "ckey", with: "http")
                
                if let url = URL(string: newUrl), let data = try? Data(contentsOf: url) {
                    
                    /// 將數(shù)據(jù)塞給系統(tǒng)
                    loadingRequest.dataRequest?.respond(with: data)
                    
                    /// 通知系統(tǒng)請(qǐng)求結(jié)束
                    loadingRequest.finishLoading()
                    
                } else {
                    
                    /// 通知系統(tǒng)請(qǐng)求結(jié)束,請(qǐng)求有誤
                    self.finishLoadingError(loadingRequest)
                }
            }
            
            /// 通知系統(tǒng)等待
            return true
        }
        
        /// 通知系統(tǒng)不用等待
        return false
    }
    
    /// 為了演示,模擬同步網(wǎng)絡(luò)請(qǐng)求,網(wǎng)絡(luò)請(qǐng)求獲取的是數(shù)據(jù)Data
    func M3u8Request(_ url: String) -> Data? {
        let semaphore = DispatchSemaphore(value: 0)
        var result: Data? = nil
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            /// 模擬后臺(tái)替換字符串
            let newString = apple_m3u8.replacingOccurrences(of: "BipBop", with: "替換字符串")
            result = newString.data(using: .utf8)
            semaphore.signal()
        }
        _ = semaphore.wait(timeout: .distantFuture)
        return result
    }
    
    /// 請(qǐng)求失敗的,全部返回Error
    func finishLoadingError(_ loadingRequest: AVAssetResourceLoadingRequest) {
        loadingRequest.finishLoading(with: NSError(domain: NSURLErrorDomain, code: 400, userInfo: nil) as Error)
    }
    
    /// 生成AVPlayerItem
    public func playerItem(with url: String) -> AVPlayerItem {
        
        /// 直接用虛假的m3u8(m3u8_url_vir)進(jìn)行初始化,原因是:
        
        /// 外界傳進(jìn)來(lái)的url有可能不是以.m3u8結(jié)尾的,即不是m3u8格式的鏈接,如果直接用url進(jìn)行初始化,那么代理方法攔截時(shí),系統(tǒng)不會(huì)以m3u8文件格式去處理攔截的url,就是系統(tǒng)只會(huì)發(fā)起一次網(wǎng)絡(luò)請(qǐng)求,之后的操作完全無(wú)效,而用虛假的m3u8鏈接,是為了混淆系統(tǒng),讓系統(tǒng)直接認(rèn)為我們請(qǐng)求的鏈接就是m3u8格式的鏈接,那么代理里面的攔截就會(huì)執(zhí)行下去,真正的請(qǐng)求鏈接通過(guò)賦值給變量m3u8_url進(jìn)行保存,只需要在代理方法里面發(fā)起真正的鏈接請(qǐng)求就行了
        
        m3u8_url = url
        
        let urlAsset = AVURLAsset(url: URL(string: m3u8_url_vir)!, options: nil)
        urlAsset.resourceLoader.setDelegate(self, queue: .main)
        let item = AVPlayerItem(asset: urlAsset)
        if #available(iOS 9.0, *) {
            item.canUseNetworkResourcesForLiveStreamingWhilePaused = true
        }
        return item
    }
}

ViewController.swift代碼

import UIKit
import AVFoundation

class ViewController: UIViewController {

    var playerItem: AVPlayerItem!
    var playerLayer: AVPlayerLayer!
    var player: AVPlayer!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        view.backgroundColor = .white
        
        playerItem = M3u8ResourceLoader.shared.playerItem(with: "")
        
        playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: .new, context: nil)
        playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.loadedTimeRanges), options: .new, context: nil)
        
        player = AVPlayer(playerItem: playerItem)
        playerLayer = AVPlayerLayer(player: player)
        playerLayer.videoGravity = .resizeAspect
        playerLayer.contentsScale = UIScreen.main.scale
        playerLayer.frame = UIScreen.main.bounds
        view.layer.insertSublayer(playerLayer, at: 0)
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == #keyPath(AVPlayerItem.loadedTimeRanges) {
            // 緩沖進(jìn)度 暫時(shí)不處理
        } else if keyPath == #keyPath(AVPlayerItem.status) {
            // 監(jiān)聽(tīng)狀態(tài)改變
            if playerItem.status == .readyToPlay {
                // 只有在這個(gè)狀態(tài)下才能播放
                player.play()
            } else {
                print("加載異常")
            }
        } else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }

    deinit {
        playerItem.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.status))
        playerItem.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.loadedTimeRanges))
    }
}

oc版鏈接: http://www.itdecent.cn/p/700a3887ff52

demo地址: https://github.com/weishenghe/MyRepository

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容