Skip to content

Commit

Permalink
Compatibility overhaul (#92)
Browse files Browse the repository at this point in the history
* Compatibility overhaul

* Use transferable TransformStream regardless of polyfill loaded after StreamSaver.js

* Allow setting WritableStream class and check for ponyfill

* Allow setting TransformStream class

* lock TransformStream

* updated the example.html with ponyfill

* Use iframes to download

* sw.js claim immediately

* now using isSecureCotext checking to decide if it should use popup or iframe to install sw.js

* Start download on popup for insecure pages not on Firefox

* Change ping interval delay on Firefox to 10secs
  • Loading branch information
TexKiller authored and jimmywarting committed Apr 12, 2019
1 parent 3a0b846 commit e5a24a7
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 85 deletions.
157 changes: 105 additions & 52 deletions StreamSaver.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* global location WritableStream ReadableStream define MouseEvent MessageChannel TransformStream */
/* global chrome location ReadableStream define MessageChannel TransformStream */

;((name, definition) => {
typeof module !== 'undefined'
? module.exports = definition()
Expand All @@ -8,40 +9,96 @@
})('streamSaver', () => {
'use strict'

const secure = location.protocol === 'https:' ||
location.protocol === 'chrome-extension:' ||
location.hostname === 'localhost'
let iframe
let loaded
let transfarableSupport = false
let streamSaver = {
let iframe, background
const test = fn => { try { fn() } catch (e) {} }
const ponyfill = window.WebStreamsPolyfill || {}
const once = { once: true }
const firefox = 'MozAppearance' in document.documentElement.style
const mozExtension = location.protocol === 'moz-extension:'
const streamSaver = {
createWriteStream,
WritableStream: window.WritableStream || ponyfill.WritableStream,
supported: false,
version: {
full: '1.2.0',
major: 1,
minor: 2,
dot: 0
}
version: { full: '1.2.0', major: 1, minor: 2, dot: 0 },
mitm: 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=1.2.0',
ping: 'https://jimmywarting.github.io/StreamSaver.js/ping.html?version=1.2.0'
}

function makeIframe (src) {
const iframe = document.createElement('iframe')
iframe.hidden = true
iframe.src = src
iframe.addEventListener('load', () => {
iframe.loaded = true
}, once)
document.body.appendChild(iframe)
return iframe
}

streamSaver.mitm = 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=' +
streamSaver.version.full
test(() => {
background = chrome.extension.getBackgroundPage() === window
})

try {
test(() => {
// Some browser has it but ain't allowed to construct a stream yet
streamSaver.supported = 'serviceWorker' in navigator && !!new ReadableStream()
} catch (err) {}
})

try {
test(() => {
// Transfariable stream was first enabled in chrome v73 behind a flag
const { readable } = new TransformStream()
const mc = new MessageChannel()
mc.port1.postMessage(readable, [readable])
mc.port1.close()
mc.port2.close()
transfarableSupport = readable.locked === true
} catch (err) {
// Was first enabled in chrome v73
// Freeze TransformStream object (can only work with native)
Object.defineProperty(streamSaver, 'TransformStream', {
configurable: false,
writable: false,
value: TransformStream
})
})

const isSecureContext = window.isSecureContext && (!firefox || !background)

function iframePostMessage (url, args) {
iframe = iframe || makeIframe(url)
if (iframe.loaded) {
iframe.contentWindow.postMessage(...args)
} else {
iframe.addEventListener('load', () => {
iframe.contentWindow.postMessage(...args)
}, once)
}
}

function load (url, noTabs, popUp) {
let popup = { close: () => (popup.closed = 1), fns: [], onLoad: fn => popup.fns.push(fn) }
if (!noTabs && window.chrome && chrome.tabs && chrome.tabs.create) {
chrome.tabs.create({ url: url, active: false }, popup2 => {
popup.close = () => chrome.tabs.remove(popup2.id)

if (popup.closed) {
popup.close()
} else {
let fn
chrome.tabs.onUpdated.addListener(fn = (tabId, _, tab) => {
if (tabId === popup2.id && tab.status === 'complete') {
chrome.tabs.onUpdated.removeListener(fn)
popup.onLoad = fn => fn()
popup.fns.forEach(popup.onLoad)
}
})
}
})
} else {
if (popUp || !firefox && !isSecureContext) {
popup = window.open(url, Math.random())
} else {
popup.close = (x => () => x.remove())(makeIframe(url))
}
}
return popup
}

function createWriteStream (filename, queuingStrategy, size) {
Expand All @@ -52,6 +109,7 @@

let channel = new MessageChannel()
let popup
let hash = ''
let setupChannel = readableStream => new Promise(resolve => {
const args = [ { filename, size }, '*', [ channel.port2 ] ]

Expand All @@ -66,48 +124,38 @@
// we recive the readable link (stream)
if (evt.data.download) {
resolve() // Signal that the writestream are ready to recive data
if (!secure) popup.close() // don't need the popup any longer
if (window.chrome && chrome.extension &&
chrome.extension.getBackgroundPage &&
chrome.extension.getBackgroundPage() === window) {
chrome.tabs.create({ url: evt.data.download, active: false })
} else {
window.location = evt.data.download
if (popup) {
if (!hash && !iframe && firefox) {
iframePostMessage(streamSaver.ping, [evt.data, '*'])
}
popup.close() // don't need the popup any longer
}
popup = load(evt.data.download, isSecureContext)

// Cleanup
if (readableStream) {
// We don't need postMessages now when stream are transferable
channel.port1.close()
channel.port2.close()
}
} else {
if (popup) {
if (firefox) popup.close()
popup = null
}

channel.port1.onmessage = null
}
}

if (secure && !iframe) {
iframe = document.createElement('iframe')
iframe.src = streamSaver.mitm
iframe.hidden = true
document.body.appendChild(iframe)
if (isSecureContext) {
return iframePostMessage(streamSaver.mitm, args)
}

if (secure && !loaded) {
let fn
iframe.addEventListener('load', fn = () => {
loaded = true
iframe.removeEventListener('load', fn)
iframe.contentWindow.postMessage(...args)
})
if (!hash && mozExtension && !streamSaver.transformStream) {
hash = '#' + Math.random()
}

if (secure && loaded) {
iframe.contentWindow.postMessage(...args)
}

if (!secure) {
popup = window.open(streamSaver.mitm, Math.random())
popup = load(streamSaver.mitm + hash, !hash, true)
if (popup.postMessage) {
let onready = evt => {
if (evt.source === popup) {
popup.postMessage(...args)
Expand All @@ -119,11 +167,16 @@
// so popup.onload() don't work but postMessage still dose
// work cross origin
window.addEventListener('message', onready)
} else {
popup.onLoad(() => {
args[0].hash = hash
iframePostMessage(streamSaver.ping, args)
})
}
})

if (transfarableSupport) {
const ts = new TransformStream({
if (streamSaver.TransformStream) {
const ts = new streamSaver.TransformStream({
start () {
return new Promise(resolve =>
setTimeout(() => setupChannel(ts.readable).then(resolve))
Expand All @@ -134,7 +187,7 @@
return ts.writable
}

return new WritableStream({
return new streamSaver.WritableStream({
start () {
// is called immediately, and should perform any actions
// necessary to acquire access to the underlying sink.
Expand Down
14 changes: 7 additions & 7 deletions example.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ <h3>What would you like to save?</h3>
</div>
<button id="$close" hidden title="When you are done writing">Close</button>
<button id="$abort" hidden title="When you want to abort">Abort</button>
<script src="StreamSaver.js"></script>
<script src="https://cdn.jsdelivr.net/webtorrent/latest/webtorrent.min.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@1.3.2/dist/polyfill.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
<script src="https://rawgit.com/jimmywarting/browser-su/master/build/permissions.js"></script>
<script src="StreamSaver.js"></script>

<script>
!streamSaver.supported && prompt(
Expand Down Expand Up @@ -73,9 +73,9 @@ <h3>What would you like to save?</h3>
permission,
writer,
url = "https://d8d913s460fub.cloudfront.net/videoserver/cat-test-video-320x240.mp4",
fileStream = streamSaver.createWriteStream(filename)
fileStream = kind !== 'torrent' && streamSaver.createWriteStream(filename)

writer = fileStream.getWriter()
writer = kind !== 'torrent' && fileStream.getWriter()
$abort.onclick = event => writer.abort()
$close.onclick = event => writer.close()

Expand Down Expand Up @@ -170,8 +170,8 @@ <h3>What would you like to save?</h3>
})

const file = torrent.files[5]
let myFile = streamSaver.createWriteStream(file.name, file.length)
let writer = myFile.getWriter()
fileStream = streamSaver.createWriteStream(file.name, file.length)
writer = fileStream.getWriter()

file.createReadStream()
.on('data', data => writer.write(data))
Expand All @@ -192,7 +192,7 @@ <h3>What would you like to save?</h3>
mediaRecorder.stop()
setTimeout(()=>{
chunks.then(evt => {
myFile.close()
fileStream.close()
})
}, 1000)
}
Expand Down
75 changes: 53 additions & 22 deletions mitm.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
to open up a link that will start the download
-->
<script>
let host = 'jimmywarting.github.io'
const host = 'jimmywarting.github.io'

// Service worker only works on https, 127.0.0.1 and localhost
// So we just redirect asap
Expand All @@ -21,35 +21,65 @@
// This will prevent the sw from restarting
let keepAlive = sw => {
keepAlive = () => {}
if (window.location.hash) {
let channel = new MessageChannel
channel.port1.onmessage = evt => {
sw.postMessage(evt.data, evt.ports)
}
new SharedWorker('ping.js').port.postMessage({
hash: window.location.hash,
ping: sw.scriptURL.substr(0, sw.scriptURL.length - 5) + 'ping'
}, [channel.port2])
}
setInterval(() => {
sw.postMessage('ping', [new MessageChannel().port2])
}, 29E3) // 29sec
}, 'MozAppearance' in document.documentElement.style ? 10E3 : 27E4)
// 10sec on Firefox, 4.5min everywhere else
}

// message event is the first thing we need to setup a listner for
// don't want the opener to do a random timeout - instead they can listen for
// the ready event
window.onmessage = event => {
let { data, ports } = event
// but since we need to wait for the Service Worker registration, we store the
// message for later
let messages = []
window.onmessage = evt => messages.push(evt)

// Register the worker, then forward the dataChannel to the worker
// So they can talk directly, so we don't have to be "the middle man" any
// longer
navigator.serviceWorker.getRegistration('./').then(swReg => {
return swReg || navigator.serviceWorker.register('sw.js', { scope: './' })
}).then(swReg => {
const swRegTmp = swReg.installing || swReg.waiting

// As soon as the Service Worker is registered we start pinging it to keep
// it alive
if (swReg.active) {
keepAlive(swReg.active)
} else {
let fn
swRegTmp.addEventListener('statechange', fn = () => {
if (swRegTmp.state === 'activated') {
swRegTmp.removeEventListener('statechange', fn)
keepAlive(swReg.active)
}
})
}

// It's important to have a messageChannel, don't want to interfere
// with other simultaneous downloads
if (!ports || !ports.length) { throw new TypeError("Mehhh! You didn't send a messageChannel") }
// Now that we have the Service Worker registered we can process messages
window.onmessage = event => {
let { data, ports } = event

// It's important to have a messageChannel, don't want to interfere
// with other simultaneous downloads
if (!ports || !ports.length) { throw new TypeError("Mehhh! You didn't send a messageChannel") }

// Register the worker, then forward the dataChannel to the worker
// So they can talk directly, so we don't have to be "the middle man" any
// longer
navigator.serviceWorker.getRegistration('./').then(swReg => {
return swReg || navigator.serviceWorker.register('sw.js', { scope: './' })
}).then(swReg => {
// This sends the message data as well as transferring
// messageChannel.port2 to the service worker. The service worker can
// then use the transferred port to reply via postMessage(), which
// will in turn trigger the onmessage handler on messageChannel.port1.
const swRegTmp = swReg.installing || swReg.waiting

if (swReg.active) {
keepAlive(swReg.active)
return swReg.active.postMessage(
data,
data.readableStream
Expand All @@ -58,20 +88,21 @@
)
}

swRegTmp.onstatechange = () => {
let fn
swRegTmp.addEventListener('statechange', fn = () => {
if (swRegTmp.state === 'activated') {
swRegTmp.onstatechange = null
swRegTmp.removeEventListener('statechange', fn)
swReg.active.postMessage(
data,
data.readableStream
? [ ports[0], data.readableStream ]
: [ ports[0] ]
)
keepAlive(swReg.active)
}
}
})
}
})
}
messages.forEach(window.onmessage)
})

// The opener can't listen to onload event, so we need to help em out!
// (telling them that we are ready to accept postMessage's)
Expand Down
Loading

0 comments on commit e5a24a7

Please sign in to comment.