|
| 1 | +'use strict' |
| 2 | + |
| 3 | +/* eslint-env browser */ |
| 4 | + |
| 5 | +const Ipfs = require('ipfs') |
| 6 | +const videoStream = require('videostream') |
| 7 | +const repoPath = 'ipfs-' + Math.random() |
| 8 | +const ipfs = new Ipfs({ repo: repoPath }) |
| 9 | +const through = require('through') |
| 10 | +const unixFs = require('ipfs-unixfs') |
| 11 | + |
| 12 | +const log = (line) => { |
| 13 | + document.getElementById('output').appendChild(document.createTextNode(`${line}\r\n`)) |
| 14 | +} |
| 15 | + |
| 16 | +log('Initialising IPFS') |
| 17 | + |
| 18 | +let cleanUp = () => {} |
| 19 | + |
| 20 | +ipfs.on('ready', () => { |
| 21 | + const videoElement = document.getElementById('video') |
| 22 | + videoElement.addEventListener('loadedmetadata', () => { |
| 23 | + log('Video metadata loaded') |
| 24 | + |
| 25 | + videoElement.play() |
| 26 | + }) |
| 27 | + videoElement.addEventListener('loadeddata', () => { |
| 28 | + log('First video frame loaded') |
| 29 | + }) |
| 30 | + videoElement.addEventListener('loadstart', () => { |
| 31 | + log('Started loading video') |
| 32 | + }) |
| 33 | + |
| 34 | + log('Adding video file') |
| 35 | + |
| 36 | + addVideoFile('/video.mp4') |
| 37 | + .then(hash => { |
| 38 | + log(`Added file with hash ${hash}`) |
| 39 | + log('Some other hashes you can use:') |
| 40 | + log('QmaSwSN1UkRszNu34rQ4JrbLxnX2LbyFAJnygAWmNWxt5C') |
| 41 | + log(`Choose a streaming option from the left and click 'Go!'`) |
| 42 | + |
| 43 | + const hashInput = document.getElementById('hash') |
| 44 | + const goButton = document.getElementById('gobutton') |
| 45 | + const byStream = document.getElementById('readablestream') |
| 46 | + const byData = document.getElementById('objectdata') |
| 47 | + |
| 48 | + hashInput.value = hash |
| 49 | + |
| 50 | + goButton.onclick = function () { |
| 51 | + cleanUp() |
| 52 | + |
| 53 | + if (byStream.checked) { |
| 54 | + log('Using ipfs.files.catReadableStream to play video') |
| 55 | + |
| 56 | + cleanUp = playByStream(hashInput.value.trim(), videoElement) |
| 57 | + } else { |
| 58 | + log('Using ipfs.object.data to play video') |
| 59 | + |
| 60 | + cleanUp = playByLinks(hashInput.value.trim(), videoElement) |
| 61 | + } |
| 62 | + |
| 63 | + return false |
| 64 | + } |
| 65 | + |
| 66 | + hashInput.disabled = false |
| 67 | + goButton.disabled = false |
| 68 | + byStream.disabled = false |
| 69 | + byData.disabled = false |
| 70 | + }) |
| 71 | +}) |
| 72 | + |
| 73 | +const playByLinks = (hash, videoElement) => { |
| 74 | + let stream |
| 75 | + |
| 76 | + const cleanUp = () => { |
| 77 | + if (stream && stream.destroy) { |
| 78 | + stream.destroy() |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + ipfs.object.links(hash) |
| 83 | + .then(links => { |
| 84 | + videoStream({ |
| 85 | + createReadStream: function (opts) { |
| 86 | + // Return a readable stream that provides the bytes |
| 87 | + // between offsets "start" and "end" inclusive |
| 88 | + const start = opts.start |
| 89 | + const end = opts.end ? opts.end + 1 : undefined |
| 90 | + |
| 91 | + log(`Asked for data starting at byte ${start}`) |
| 92 | + |
| 93 | + cleanUp() |
| 94 | + |
| 95 | + stream = offsetStream(links, start, end) |
| 96 | + |
| 97 | + return stream |
| 98 | + } |
| 99 | + }, videoElement) |
| 100 | + }) |
| 101 | + .catch(error => log(error.message)) |
| 102 | + |
| 103 | + return cleanUp |
| 104 | +} |
| 105 | + |
| 106 | +const playByStream = (hash, videoElement) => { |
| 107 | + let passThroughStream |
| 108 | + let fileStream |
| 109 | + |
| 110 | + const cleanUp = () => { |
| 111 | + if (passThroughStream && passThroughStream.destroy) { |
| 112 | + passThroughStream.destroy() |
| 113 | + } |
| 114 | + |
| 115 | + if (fileStream && fileStream.destroy) { |
| 116 | + fileStream.destroy() |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + videoStream({ |
| 121 | + createReadStream: function (opts) { |
| 122 | + // Return a readable stream that provides the bytes |
| 123 | + // between offsets "start" and "end" inclusive |
| 124 | + const start = opts.start |
| 125 | + let offset = 0 |
| 126 | + |
| 127 | + log(`Asked for data starting at byte ${start}`) |
| 128 | + |
| 129 | + cleanUp() |
| 130 | + |
| 131 | + // We will write the requested bytes into this stream |
| 132 | + const passThroughStream = through() |
| 133 | + |
| 134 | + // Get the requested file from IPFS and seek through the stream util |
| 135 | + // we find the requested bytes |
| 136 | + const fileStream = ipfs.files.catReadableStream(hash) |
| 137 | + fileStream.on('data', (data) => { |
| 138 | + if (offset > start) { |
| 139 | + // If we've passed the requested start just pass everything through |
| 140 | + passThroughStream.write(data) |
| 141 | + } else if (offset + data.length > start) { |
| 142 | + // If the requested start is in the middle of our data, pass a slice through |
| 143 | + const begin = start - offset |
| 144 | + |
| 145 | + passThroughStream.write(data.slice(begin)) |
| 146 | + } |
| 147 | + |
| 148 | + offset += data.length |
| 149 | + }) |
| 150 | + |
| 151 | + return passThroughStream |
| 152 | + } |
| 153 | + }, videoElement) |
| 154 | + |
| 155 | + return cleanUp |
| 156 | +} |
| 157 | + |
| 158 | +const offsetStream = (links, startByte, endByte) => { |
| 159 | + // We will write the requested bytes into this stream |
| 160 | + const stream = through() |
| 161 | + let streamPosition = 0 |
| 162 | + |
| 163 | + series(links.map((link, index) => { |
| 164 | + return () => { |
| 165 | + if (!stream.writable) { |
| 166 | + // The stream has been closed |
| 167 | + return |
| 168 | + } |
| 169 | + |
| 170 | + // DAGNode Links report unixfs object data sizes 14 bytes larger due to the protobuf wrapper |
| 171 | + const bytesInLinkedObjectData = link.size - 14 |
| 172 | + |
| 173 | + if (startByte > (streamPosition + bytesInLinkedObjectData)) { |
| 174 | + // Start byte is after this block so skip it |
| 175 | + streamPosition += bytesInLinkedObjectData |
| 176 | + |
| 177 | + return |
| 178 | + } |
| 179 | + |
| 180 | + if (endByte && endByte < streamPosition) { |
| 181 | + // End byte was before this block so skip it |
| 182 | + streamPosition += bytesInLinkedObjectData |
| 183 | + |
| 184 | + return |
| 185 | + } |
| 186 | + |
| 187 | + return ipfs.object.data(link.multihash) |
| 188 | + .then(data => unixFs.unmarshal(data).data) |
| 189 | + .then(data => { |
| 190 | + if (!stream.writable) { |
| 191 | + // The stream was closed while we were getting data |
| 192 | + return |
| 193 | + } |
| 194 | + |
| 195 | + const length = data.length |
| 196 | + |
| 197 | + if (startByte > streamPosition && startByte < (streamPosition + length)) { |
| 198 | + // If the startByte is in the current block, skip to the startByte |
| 199 | + data = data.slice(startByte - streamPosition) |
| 200 | + } |
| 201 | + |
| 202 | + stream.write(data) |
| 203 | + |
| 204 | + streamPosition += length |
| 205 | + }) |
| 206 | + } |
| 207 | + })) |
| 208 | + .catch(error => log(error.message)) |
| 209 | + |
| 210 | + return stream |
| 211 | +} |
| 212 | + |
| 213 | +const addVideoFile = (path) => { |
| 214 | + return fetch(path) |
| 215 | + .then(response => response.arrayBuffer()) |
| 216 | + .then(buffer => { |
| 217 | + return ipfs.files.add({ |
| 218 | + path, |
| 219 | + content: ipfs.types.Buffer.from(buffer) |
| 220 | + }) |
| 221 | + }) |
| 222 | + .then(result => result.pop().hash) |
| 223 | +} |
| 224 | + |
| 225 | +const series = (promiseFactories) => { |
| 226 | + let result = Promise.resolve() |
| 227 | + |
| 228 | + promiseFactories.forEach((promiseFactory) => { |
| 229 | + result = result.then(promiseFactory) |
| 230 | + }) |
| 231 | + |
| 232 | + return result |
| 233 | +} |
0 commit comments