抽了點(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))
}
}