类似的工具论坛里有,自己编写主要是想进一步了解其中的门道。软件编写使用的是Electron+Vue。
【未加密视频的播放和下载】
这个比较简单,只在界面上设置了“视频地址输入”“下载目录设置”“播放按钮”“下载按钮”以及“播放区域”。
播放使用的是hls.js这个插件,使用方法如下:
[JavaScript] 纯文本查看 复制代码 this.hlsVideoObj = new HLS()
this.hlsVideoObj.attachMedia(this.$refs.video)
this.hlsVideoObj.on(HLS.Events.MEDIA_ATTACHED, () => {
this.hlsVideoObj.loadSource(this.videoUrl)
})
this.$nextTick(() => {
this.$refs.video.play()
})
下载功能是先使用fetch请求到m3u8的文件,然后使用m3u8-parser插件对文件内容进行解析,得到ts文件的列表,然后循环下载ts资源。待所有ts文件下载完成以后将它们合并并输出到本地下载目录。
[JavaScript] 纯文本查看 复制代码 downloadTs () {
let urlArr = this.videoUrl.split('?')[0].split('/')
let baseUrl = urlArr.slice(0, urlArr.length - 1).join('/')
let segments = this.parsedManifest.segments || []
let fileName = urlArr[urlArr.length - 1] + '.mp4'
this.taskList = []
this.dataList = []
this.downloadingCount = segments.length
segments.forEach((v, i) => {
let tsUrl = `${baseUrl}/${v.uri}`
this.taskList.push(
fetch(tsUrl, {method: 'get', responseType: 'arraybuffer'}).then(res => {
return res.arrayBuffer()
}).then((ab) => {
this.dataList[i] = Buffer.from(ab)
}).catch((err) => {
console.log(`segment ${i}:`, err)
}).finally(() => {
this.downloadingCount--
})
)
})
Promise.all(this.taskList).then(() => {
let filePath = path.join(this.toPath, fileName)
let dataList = this.dataList.filter(v => v)
let data = Buffer.concat(dataList)
this.$sharedObject('jstoolsNode').writeFile(filePath, data).then(() =>{
this.$toast('下载成功')
this.taskList = []
this.dataList = []
}).catch((err) => {
this.$toast('下载失败' + err)
})
})
}
【加密视频的播放和下载】
加密视频因为涉及到解密,所以会增加很多配置项,所以界面会相应变得复杂一些。
对于加密视频,首先有个套路,就是基本都会对请求源做校验,所以要先修改请求头,这里使用Electron的session模块拦截修改所有请求的referer
[JavaScript] 纯文本查看 复制代码 setOriginUrl () {
let urlObj = {}
try { urlObj = new URL(this.videoUrl) } catch (err) { console.log(err) }
let origin = urlObj.origin || '*://*/*'
let filters = {urls: [`${origin}/*`]}
session.defaultSession.webRequest.onBeforeSendHeaders(filters, (details, callback) => {
details.requestHeaders['Origin'] = origin
details.requestHeaders['Referer'] = this.refererUrl
callback({cancel: false, requestHeaders: details.requestHeaders})
})
this.$toast('请求源设置已生效')
}
接着,在m3u8中找到key对应的地址获取到key,这里接口返回的往往是经过加密的key,这时就需要解密一次
得到真实key以后,就可以用它对下载下来的ts数据进行解密了。
[JavaScript] 纯文本查看 复制代码 decrypt (buffer, key, iv) {
key = this.key || key || ''
iv = this.iv || iv || ''
// 处理key和iv的格式
let keyWA = key
if (typeof key === 'string') {
// 密钥转字节数组(16位)
let keyBy = stringToBytes(key)
// 字节数组转Uint8Array
let keyBv = new Uint8Array(keyBy)
// Uint8Array转WordArray
keyWA = CryptoJS.enc.u8array.parse(keyBv)
}
let ivWA = iv
if (typeof iv === 'string') {
let ivBy = hexToBytes(iv)
let ivBv = new Uint8Array(ivBy)
ivWA = CryptoJS.enc.u8array.parse(ivBv)
}
let view = new Uint8Array(buffer)
// 将Uint8Array 转成 WordArray
let contentWA = CryptoJS.enc.u8array.parse(view)
// base64字符串
let dcBase64String = contentWA.toString(CryptoJS.enc.Base64)
// 解密
let decryptedData = CryptoJS.AES.decrypt(dcBase64String, keyWA, {
iv: ivWA,
mode: CryptoJS.mode[this.decryptMode],
padding: CryptoJS.pad[this.decryptPadding]
})
// 把解密后的对象再转为base64编码,这步是关键,跟解密文字不同
let d64 = decryptedData.toString(CryptoJS.enc.Base64)
return base642arraybuffer(d64)
}
待所有数据都解密完成后,就可以将他们合并然后下载到本地了。
更多详情和代码解释可以看视频展示:https://www.bilibili.com/video/BV14M411r7QX/
|