stream-saver.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. (function () {
  2. // 下载代理,使用 iframe,还是 navigate
  3. const downloadStrategy =
  4. window.isSecureContext // window.isSecureContext 判断是否为 https、wss 等安全环境
  5. || 'MozAppearance' in document.documentElement.style // 是否为 firefox 浏览器
  6. ? 'iframe' : 'navigate'
  7. // 中间传输器
  8. let middleTransporter = null
  9. // 是否使用 blob 替换 service worker 的能力
  10. // safari 不支持流式下载功能,https://github.com/jimmywarting/StreamSaver.js/issues/69
  11. let useBlobFallback = /constructor/i.test(window.HTMLElement) || !!window.safari || !!window.WebKitPoint
  12. try {
  13. new Response(new ReadableStream())
  14. if (window.isSecureContext && !('serviceWorker' in navigator)) {
  15. useBlobFallback = true
  16. }
  17. } catch (err) {
  18. useBlobFallback = true
  19. }
  20. // 是否支持转换器传输流 TransformStream,支持则直接使用他的读写流,完成下载数据的传输。都在需要通过 messageChannel 进行数据传输
  21. let isSupportTransformStream = false
  22. try {
  23. const { readable } = new TransformStream() // 创建读写传输流
  24. const messageChannel = new MessageChannel() // 创建消息通道,与 iframe 或 window.open 新建的页面中进行消息通信
  25. messageChannel.port1.postMessage(readable, [readable])
  26. messageChannel.port1.close()
  27. messageChannel.port2.close()
  28. isSupportTransformStream = true
  29. } catch (err) {
  30. console.log(err)
  31. }
  32. // 创建一个隐藏式的 Iframe,并通过 iframe 的 postMessage 进行消息传输
  33. function makeIframe(src) {
  34. console.log('makeIframe', src)
  35. const iframe = document.createElement('iframe')
  36. iframe.hidden = true
  37. iframe.src = src
  38. iframe.loaded = false
  39. iframe.name = 'iframe'
  40. iframe.isIframe = true
  41. // 调用 iframe 中的 postMessage 方法,即从 iframe 中发送消息
  42. iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args)
  43. iframe.addEventListener('load', () => {
  44. iframe.loaded = true
  45. }, { once: true }) // 该事件监听器只监听一次,自动回收
  46. document.body.appendChild(iframe)
  47. return iframe
  48. }
  49. // 创建一个弹出窗口,模拟iframe的基本功能
  50. // 使用 popup 新建弹窗,来模拟 iframe 的跨页面消息传输功能
  51. function makePopup(src) {
  52. console.log('makePopup', src)
  53. // 事件代理器,使用 createDocumentFragment 来实现 popup 中的消息监听效果。
  54. // 与 document 相比,最大的区别是它不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会对性能产生影响。
  55. const delegate = document.createDocumentFragment()
  56. const popup = {
  57. frame: window.open(src, 'popupTitle', 'width=200,height=100'),
  58. loaded: false,
  59. isIframe: false,
  60. isPopup: true,
  61. remove() { popup.frame.close() },
  62. // 适配器模式,使得 popup 对象与 iframe 对象有一样的表现。发送事件,监听事件,移除事件
  63. dispatchEvent(...args) { delegate.dispatchEvent(...args) },
  64. addEventListener(...args) { delegate.addEventListener(...args) },
  65. removeEventListener(...args) { delegate.removeEventListener(...args) },
  66. // 调用
  67. postMessage(...args) { popup.frame.postMessage(...args) }
  68. }
  69. // 监听 popup 是否就绪
  70. const onReady = evt => {
  71. // 如果接受到来自 popup 的事件,则证明 popup 已就绪
  72. if (evt.source === popup.frame) {
  73. popup.loaded = true
  74. window.removeEventListener('message', onReady)
  75. popup.dispatchEvent(new Event('load'))
  76. }
  77. }
  78. window.addEventListener('message', onReady)
  79. return popup
  80. }
  81. // 创建写入流
  82. function createWriteStream(filename) {
  83. let bytesWritten = 0 // 记录已写入的文件大小
  84. let downloadUrl = null // 触发下载时,需要访问的 url 地址
  85. let messageChannel = null // 消息传输通道
  86. let transformStream = null // 中间传输流
  87. if (!useBlobFallback) {
  88. // middleTransporter = middleTransporter || makeIframe(streamSaver.middleTransporterUrl) // https 环境下,则执行 iframe
  89. middleTransporter = middleTransporter || window.isSecureContext
  90. ? makeIframe(streamSaver.middleTransporterUrl) // https 环境下,则执行 iframe
  91. : makePopup(streamSaver.middleTransporterUrl) // 普通环境下,则通过 window.open 新建弹窗来完成
  92. messageChannel = new MessageChannel() // 创建消息通道
  93. // 处理文件名,使其为 url 格式
  94. filename = encodeURIComponent(filename.replace(/\//g, ':'))
  95. .replace(/['()]/g, escape)
  96. .replace(/\*/g, '%2A')
  97. // 如果支持 TransformStream,则将 TransformStream.readStream 传递给 port2
  98. if (isSupportTransformStream) {
  99. transformStream = new TransformStream(downloadStrategy === 'iframe' ? undefined : {
  100. // 流处理,中间转换器,监听每一个流分片的经过
  101. transform(chunk, controller) {
  102. // 传输的内容,仅支持 Uint8Arrays 格式
  103. if (!(chunk instanceof Uint8Array)) {
  104. throw new TypeError('Can only write Uint8Arrays')
  105. }
  106. bytesWritten += chunk.length // 记录已写入的内容消大小
  107. controller.enqueue(chunk) // 将消息推进队列
  108. if (downloadUrl) {
  109. location.href = downloadUrl // 由于在 response 中设置了返回类型为二进制流,可直接触发其下载。不会发生跳转
  110. downloadUrl = null
  111. }
  112. },
  113. // 结束写入时调用,如果数据量少,未经过 transform 就触发了 flush,则调用 location.href 触发下载
  114. flush() {
  115. if (downloadUrl) {
  116. location.href = downloadUrl
  117. }
  118. }
  119. })
  120. // 使用 port1 传递数据,将读数据端通过 channel Message 传递给 service worker
  121. // 由 write 暴露写端,供主线程写入数据。再在 service worker 中,通过 readStream 读取该数据。完成下载数据的传输。
  122. // 即下载数据,不需要通过 channel message 传输,而是通过 transformStream 进行传递。
  123. messageChannel.port1.postMessage({ readableStream: transformStream.readable }, [transformStream.readable])
  124. }
  125. // 监听给 port1 传递的消息
  126. messageChannel.port1.onmessage = evt => {
  127. // 接受 Service worker 发送的 url,并访问它
  128. if (evt.data.download) {
  129. // 为 popup 做的特殊处理
  130. if (downloadStrategy === 'navigate') {
  131. // 中间人完成使命,则删除中间人,后续传输通过 channelMessage,直接由主进程与 service worker 进行通信
  132. middleTransporter.remove()
  133. middleTransporter = null
  134. // 首次访问该 url
  135. if (bytesWritten) {
  136. location.href = evt.data.download
  137. } else {
  138. downloadUrl = evt.data.download
  139. }
  140. } else {
  141. if (middleTransporter.isPopup) {
  142. middleTransporter.remove()
  143. middleTransporter = null
  144. // Special case for firefox, they can keep sw alive with fetch
  145. if (downloadStrategy === 'iframe') {
  146. makeIframe(streamSaver.middleTransporterUrl)
  147. }
  148. }
  149. makeIframe(evt.data.download)
  150. }
  151. } else if (evt.data.abort) { // 消息终止
  152. chunks = []
  153. messageChannel.port1.postMessage('abort') //send back so controller is aborted
  154. messageChannel.port1.onmessage = null
  155. messageChannel.port1.close()
  156. messageChannel.port2.close()
  157. messageChannel = null
  158. }
  159. }
  160. // 往中间人容器中发送消息,将 messageChannel.port2 传递给中间人
  161. const response = {
  162. transferringReadable: isSupportTransformStream,
  163. pathname: Math.random().toString().slice(-6) + '/' + filename,
  164. headers: {
  165. 'Content-Type': 'application/octet-stream; charset=utf-8',
  166. 'Content-Disposition': "attachment; filename*=UTF-8''" + filename
  167. }
  168. }
  169. if (middleTransporter.loaded) {
  170. middleTransporter.postMessage(response, '*', [messageChannel.port2])
  171. } else {
  172. middleTransporter.addEventListener('load', () => {
  173. middleTransporter.postMessage(response, '*', [messageChannel.port2])
  174. }, { once: true })
  175. }
  176. }
  177. let chunks = [] // 需要传输下载的内容数组
  178. // 如果有 transformStream,则直接返回 transformStream 读写流的 WritableStream 实例
  179. if (!useBlobFallback && transformStream && transformStream.writable) {
  180. // writable 返回由这个 TransformStream 控制的 WritableStream 实例。
  181. // writable 返回的是一个实例,而不是一个 boolean 值
  182. return transformStream.writable
  183. }
  184. // 如果不支持 transformStream,则自行创建一个 WritableStream,监听 WritableStream 的写入事件。将数据通过 messageChannel 的两个 port 进行传输
  185. return new WritableStream({
  186. // 写入数据
  187. write(chunk) {
  188. // 检查写入流,仅支持 Uint8Arrays 格式
  189. if (!(chunk instanceof Uint8Array)) {
  190. throw new TypeError('Can only write Uint8Arrays')
  191. }
  192. // 如果使用 blob 功能进行下载,则仅存储该数据,无法使用流式边获取数据边下载
  193. if (useBlobFallback) {
  194. chunks.push(chunk)
  195. return
  196. }
  197. // service worker 可用,则通过信道传输该二进制流
  198. messageChannel.port1.postMessage(chunk)
  199. bytesWritten += chunk.length
  200. if (downloadUrl) {
  201. location.href = downloadUrl
  202. downloadUrl = null
  203. }
  204. },
  205. // 关闭写入流,将流式文件进行保存
  206. close() {
  207. // 使用 blob 实现功能,则将所有片段当做 blob 的内容,通过 createObjectURL 生成其链接,点击触发下载
  208. if (useBlobFallback) {
  209. const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' })
  210. const link = document.createElement('a')
  211. link.href = URL.createObjectURL(blob)
  212. link.download = filename
  213. link.click()
  214. } else { // service worker 有效,则仅发出 end 事件,由 service worker 执行结束操作
  215. messageChannel.port1.postMessage('end')
  216. }
  217. },
  218. // 中断,不执行下载
  219. abort() {
  220. chunks = []
  221. messageChannel.port1.postMessage('abort')
  222. messageChannel.port1.onmessage = null
  223. messageChannel.port1.close()
  224. messageChannel.port2.close()
  225. messageChannel = null
  226. }
  227. })
  228. }
  229. // 全局挂载 streamSaver 对象
  230. window.streamSaver = {
  231. createWriteStream, // 创建写流
  232. middleTransporterUrl: 'https://live.fanmingming.com/m3u8/mitm.html',
  233. }
  234. })()