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ū)之類(lèi)的曹动,用Alamofire請(qǐng)求吧斋日,同上面的m3u8一樣,也要同步
                
                /// 在這里對(duì)字符串進(jìn)行任意修改墓陈,解密之類(lèi)的恶守,同上
                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.reibang.com/p/700a3887ff52

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末崭闲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子威蕉,更是在濱河造成了極大的恐慌刁俭,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件韧涨,死亡現(xiàn)場(chǎng)離奇詭異牍戚,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)虑粥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)如孝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人娩贷,你說(shuō)我怎么就攤上這事第晰。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵茁瘦,是天一觀的道長(zhǎng)品抽。 經(jīng)常有香客問(wèn)我,道長(zhǎng)甜熔,這世上最難降的妖魔是什么圆恤? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮纺非,結(jié)果婚禮上哑了,老公的妹妹穿的比我還像新娘赘方。我一直安慰自己烧颖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布窄陡。 她就那樣靜靜地躺著炕淮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪跳夭。 梳的紋絲不亂的頭發(fā)上涂圆,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音币叹,去河邊找鬼润歉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛颈抚,可吹牛的內(nèi)容都是我干的踩衩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼贩汉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼驱富!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起匹舞,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤褐鸥,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后赐稽,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體叫榕,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年姊舵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了晰绎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蠢莺,死狀恐怖寒匙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤锄弱,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布考蕾,位于F島的核電站,受9級(jí)特大地震影響会宪,放射性物質(zhì)發(fā)生泄漏肖卧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一掸鹅、第九天 我趴在偏房一處隱蔽的房頂上張望塞帐。 院中可真熱鬧,春花似錦巍沙、人聲如沸葵姥。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)榔幸。三九已至,卻和暖如春矮嫉,著一層夾襖步出監(jiān)牢的瞬間削咆,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工蠢笋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拨齐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓昨寞,卻偏偏與公主長(zhǎng)得像瞻惋,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子编矾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348