1 Star 0 Fork 0

tea-sea-sun/m3u8-downloader

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
StreamSaver.js 10.91 KB
一键复制 编辑 原始数据 按行查看 历史
(function () {
// 下载代理,使用 iframe,还是 navigate
const downloadStrategy =
window.isSecureContext // window.isSecureContext 判断是否为 https、wss 等安全环境
|| 'MozAppearance' in document.documentElement.style // 是否为 firefox 浏览器
? 'iframe' : 'navigate'
// 中间传输器
let middleTransporter = null
// 是否使用 blob 替换 service worker 的能力
// safari 不支持流式下载功能,https://github.com/jimmywarting/StreamSaver.js/issues/69
let useBlobFallback = /constructor/i.test(window.HTMLElement) || !!window.safari || !!window.WebKitPoint
try {
new Response(new ReadableStream())
if (window.isSecureContext && !('serviceWorker' in navigator)) {
useBlobFallback = true
}
} catch (err) {
useBlobFallback = true
}
// 是否支持转换器传输流 TransformStream,支持则直接使用他的读写流,完成下载数据的传输。都在需要通过 messageChannel 进行数据传输
let isSupportTransformStream = false
try {
const { readable } = new TransformStream() // 创建读写传输流
const messageChannel = new MessageChannel() // 创建消息通道,与 iframe 或 window.open 新建的页面中进行消息通信
messageChannel.port1.postMessage(readable, [readable])
messageChannel.port1.close()
messageChannel.port2.close()
isSupportTransformStream = true
} catch (err) {
console.log(err)
}
// 创建一个隐藏式的 Iframe,并通过 iframe 的 postMessage 进行消息传输
function makeIframe(src) {
console.log('makeIframe', src)
const iframe = document.createElement('iframe')
iframe.hidden = true
iframe.src = src
iframe.loaded = false
iframe.name = 'iframe'
iframe.isIframe = true
// 调用 iframe 中的 postMessage 方法,即从 iframe 中发送消息
iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args)
iframe.addEventListener('load', () => {
iframe.loaded = true
}, { once: true }) // 该事件监听器只监听一次,自动回收
document.body.appendChild(iframe)
return iframe
}
// 创建一个弹出窗口,模拟iframe的基本功能
// 使用 popup 新建弹窗,来模拟 iframe 的跨页面消息传输功能
function makePopup(src) {
console.log('makePopup', src)
// 事件代理器,使用 createDocumentFragment 来实现 popup 中的消息监听效果。
// 与 document 相比,最大的区别是它不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会对性能产生影响。
const delegate = document.createDocumentFragment()
const popup = {
frame: window.open(src, 'popupTitle', 'width=200,height=100'),
loaded: false,
isIframe: false,
isPopup: true,
remove() { popup.frame.close() },
// 适配器模式,使得 popup 对象与 iframe 对象有一样的表现。发送事件,监听事件,移除事件
dispatchEvent(...args) { delegate.dispatchEvent(...args) },
addEventListener(...args) { delegate.addEventListener(...args) },
removeEventListener(...args) { delegate.removeEventListener(...args) },
// 调用
postMessage(...args) { popup.frame.postMessage(...args) }
}
// 监听 popup 是否就绪
const onReady = evt => {
// 如果接受到来自 popup 的事件,则证明 popup 已就绪
if (evt.source === popup.frame) {
popup.loaded = true
window.removeEventListener('message', onReady)
popup.dispatchEvent(new Event('load'))
}
}
window.addEventListener('message', onReady)
return popup
}
// 创建写入流
function createWriteStream(filename) {
let bytesWritten = 0 // 记录已写入的文件大小
let downloadUrl = null // 触发下载时,需要访问的 url 地址
let messageChannel = null // 消息传输通道
let transformStream = null // 中间传输流
if (!useBlobFallback) {
// middleTransporter = middleTransporter || makeIframe(streamSaver.middleTransporterUrl) // https 环境下,则执行 iframe
middleTransporter = middleTransporter || window.isSecureContext
? makeIframe(streamSaver.middleTransporterUrl) // https 环境下,则执行 iframe
: makePopup(streamSaver.middleTransporterUrl) // 普通环境下,则通过 window.open 新建弹窗来完成
messageChannel = new MessageChannel() // 创建消息通道
// 处理文件名,使其为 url 格式
filename = encodeURIComponent(filename.replace(/\//g, ':'))
.replace(/['()]/g, escape)
.replace(/\*/g, '%2A')
// 如果支持 TransformStream,则将 TransformStream.readStream 传递给 port2
if (isSupportTransformStream) {
transformStream = new TransformStream(downloadStrategy === 'iframe' ? undefined : {
// 流处理,中间转换器,监听每一个流分片的经过
transform(chunk, controller) {
// 传输的内容,仅支持 Uint8Arrays 格式
if (!(chunk instanceof Uint8Array)) {
throw new TypeError('Can only write Uint8Arrays')
}
bytesWritten += chunk.length // 记录已写入的内容消大小
controller.enqueue(chunk) // 将消息推进队列
if (downloadUrl) {
location.href = downloadUrl // 由于在 response 中设置了返回类型为二进制流,可直接触发其下载。不会发生跳转
downloadUrl = null
}
},
// 结束写入时调用,如果数据量少,未经过 transform 就触发了 flush,则调用 location.href 触发下载
flush() {
if (downloadUrl) {
location.href = downloadUrl
}
}
})
// 使用 port1 传递数据,将读数据端通过 channel Message 传递给 service worker
// 由 write 暴露写端,供主线程写入数据。再在 service worker 中,通过 readStream 读取该数据。完成下载数据的传输。
// 即下载数据,不需要通过 channel message 传输,而是通过 transformStream 进行传递。
messageChannel.port1.postMessage({ readableStream: transformStream.readable }, [transformStream.readable])
}
// 监听给 port1 传递的消息
messageChannel.port1.onmessage = evt => {
// 接受 Service worker 发送的 url,并访问它
if (evt.data.download) {
// 为 popup 做的特殊处理
if (downloadStrategy === 'navigate') {
// 中间人完成使命,则删除中间人,后续传输通过 channelMessage,直接由主进程与 service worker 进行通信
middleTransporter.remove()
middleTransporter = null
// 首次访问该 url
if (bytesWritten) {
location.href = evt.data.download
} else {
downloadUrl = evt.data.download
}
} else {
if (middleTransporter.isPopup) {
middleTransporter.remove()
middleTransporter = null
// Special case for firefox, they can keep sw alive with fetch
if (downloadStrategy === 'iframe') {
makeIframe(streamSaver.middleTransporterUrl)
}
}
makeIframe(evt.data.download)
}
} else if (evt.data.abort) { // 消息终止
chunks = []
messageChannel.port1.postMessage('abort') //send back so controller is aborted
messageChannel.port1.onmessage = null
messageChannel.port1.close()
messageChannel.port2.close()
messageChannel = null
}
}
// 往中间人容器中发送消息,将 messageChannel.port2 传递给中间人
const response = {
transferringReadable: isSupportTransformStream,
pathname: Math.random().toString().slice(-6) + '/' + filename,
headers: {
'Content-Type': 'application/octet-stream; charset=utf-8',
'Content-Disposition': "attachment; filename*=UTF-8''" + filename
}
}
if (middleTransporter.loaded) {
middleTransporter.postMessage(response, '*', [messageChannel.port2])
} else {
middleTransporter.addEventListener('load', () => {
middleTransporter.postMessage(response, '*', [messageChannel.port2])
}, { once: true })
}
}
let chunks = [] // 需要传输下载的内容数组
// 如果有 transformStream,则直接返回 transformStream 读写流的 WritableStream 实例
if (!useBlobFallback && transformStream && transformStream.writable) {
// writable 返回由这个 TransformStream 控制的 WritableStream 实例。
// writable 返回的是一个实例,而不是一个 boolean 值
return transformStream.writable
}
// 如果不支持 transformStream,则自行创建一个 WritableStream,监听 WritableStream 的写入事件。将数据通过 messageChannel 的两个 port 进行传输
return new WritableStream({
// 写入数据
write(chunk) {
// 检查写入流,仅支持 Uint8Arrays 格式
if (!(chunk instanceof Uint8Array)) {
throw new TypeError('Can only write Uint8Arrays')
}
// 如果使用 blob 功能进行下载,则仅存储该数据,无法使用流式边获取数据边下载
if (useBlobFallback) {
chunks.push(chunk)
return
}
// service worker 可用,则通过信道传输该二进制流
messageChannel.port1.postMessage(chunk)
bytesWritten += chunk.length
if (downloadUrl) {
location.href = downloadUrl
downloadUrl = null
}
},
// 关闭写入流,将流式文件进行保存
close() {
// 使用 blob 实现功能,则将所有片段当做 blob 的内容,通过 createObjectURL 生成其链接,点击触发下载
if (useBlobFallback) {
const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.click()
} else { // service worker 有效,则仅发出 end 事件,由 service worker 执行结束操作
messageChannel.port1.postMessage('end')
}
},
// 中断,不执行下载
abort() {
chunks = []
messageChannel.port1.postMessage('abort')
messageChannel.port1.onmessage = null
messageChannel.port1.close()
messageChannel.port2.close()
messageChannel = null
}
})
}
// 全局挂载 streamSaver 对象
window.streamSaver = {
createWriteStream, // 创建写流
middleTransporterUrl: 'https://blog.luckly-mjw.cn/tool-show/m3u8-downloader/mitm.html',
// middleTransporterUrl: 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0'
}
})()
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Java
1
https://gitee.com/tea-sea-sun/m3u8-downloader.git
git@gitee.com:tea-sea-sun/m3u8-downloader.git
tea-sea-sun
m3u8-downloader
m3u8-downloader
master

搜索帮助