Skip to content

Commit c18ddf1

Browse files
committed
Accept stream instances instead of readers
1 parent fd35930 commit c18ddf1

File tree

7 files changed

+57
-73
lines changed

7 files changed

+57
-73
lines changed

demos/browser/video.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,6 @@ <h3>Uploads</h3>
6262
</div>
6363
</body>
6464

65-
<script src="../../dist/tus.js"></script>
65+
<script src="../../../dist/tus.js"></script>
6666
<script src="./video.js"></script>
6767
</html>

demos/browser/video.js

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ function startUpload(file) {
3434
}
3535

3636
const options = {
37-
resume: false,
3837
endpoint,
3938
chunkSize,
4039
retryDelays: [0, 1000, 3000, 5000],
@@ -87,52 +86,44 @@ function startUpload(file) {
8786
function startStreamUpload() {
8887
navigator.mediaDevices
8988
.getUserMedia({ video: true })
90-
.then((stream) => {
91-
const mr = new MediaRecorder(stream)
92-
const chunks = []
93-
let done = false
94-
let onDataAvailable = null
95-
96-
mr.onerror = onError
97-
mr.onstop = () => {
98-
done = true
99-
if (onDataAvailable) onDataAvailable(readableRecorder.read())
100-
}
101-
mr.ondataavailable = (event) => {
102-
chunks.push(event.data)
103-
if (onDataAvailable) {
104-
onDataAvailable(readableRecorder.read())
105-
onDataAvailable = undefined
89+
.then((mediaStream) => {
90+
const mr = new MediaRecorder(mediaStream)
91+
92+
stopRecording = () => {
93+
for (const t of mediaStream.getTracks()) {
94+
t.stop()
10695
}
96+
mr.stop()
97+
stopRecording = null
10798
}
10899

109-
mr.start(1000)
100+
const readableStream = new ReadableStream({
101+
start(controller) {
102+
mr.ondataavailable = (event) => {
103+
event.data.bytes().then((buffer) => {
104+
controller.enqueue(buffer)
105+
})
106+
}
110107

111-
const readableRecorder = {
112-
read() {
113-
if (done && chunks.length === 0) {
114-
return Promise.resolve({ done: true })
108+
mr.onstop = () => {
109+
controller.close()
115110
}
116111

117-
if (chunks.length > 0) {
118-
return Promise.resolve({ value: chunks.shift(), done: false })
112+
mr.onerror = (event) => {
113+
controller.error(event.error)
119114
}
120115

121-
return new Promise((resolve) => {
122-
onDataAvailable = resolve
123-
})
116+
mr.start(1000)
124117
},
125-
}
126-
127-
startUpload(readableRecorder)
118+
pull(_controller) {
119+
// Nothing to do here. MediaRecorder let's us know when there is new data available.
120+
},
121+
cancel() {
122+
mr.stop()
123+
},
124+
})
128125

129-
stopRecording = () => {
130-
for (const t of stream.getTracks()) {
131-
t.stop()
132-
}
133-
mr.stop()
134-
stopRecording = null
135-
}
126+
startUpload(readableStream)
136127
})
137128
.catch(onError)
138129
}

docs/api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ A boolean indicating if the fingerprint in the URL storage will be removed once
224224

225225
_Default value:_ `false`
226226

227-
A boolean indicating whether a stream of data is going to be uploaded as a [`Reader`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader). If so, the total size isn't available when we begin uploading, so we use the Tus [`Upload-Defer-Length`](https://tus.io/protocols/resumable-upload.html#upload-defer-length) header. Once the reader is finished, the total file size is sent to the tus server in order to complete the upload. Furthermore, `chunkSize` must be set to a finite number. See the `/demos/browser/video.js` file for an example of how to use this property.
227+
A boolean indicating whether a stream of data is going to be uploaded as a [`Reader`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream). If so, the total size isn't available when we begin uploading, so we use the Tus [`Upload-Defer-Length`](https://tus.io/protocols/resumable-upload.html#upload-defer-length) header. Once the reader is finished, the total file size is sent to the tus server in order to complete the upload. Furthermore, `chunkSize` must be set to a finite number. See the `/demos/browser/video.js` file for an example of how to use this property.
228228

229229
#### uploadDataDuringCreation
230230

@@ -442,7 +442,7 @@ The constructor for the `tus.Upload` class. The upload will not be started autom
442442

443443
Depending on the platform, the `file` argument must be an instance of the following types:
444444

445-
- inside browser: `File`, `Blob`, or [`Reader`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader)
445+
- inside browser: `File`, `Blob`, or [`Reader`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
446446
- inside Node.js: `Buffer` or `Readable` stream
447447
- inside Cordova: `File` object from a `FileEntry` (see [demo](/demos/cordova/www/js/index.js))
448448
- inside React Native: Object with uri property: `{ uri: 'file:///...', ... }` (see [installation notes](/docs/installation.md#react-native-support) and [demo](/demos/reactnative/App.js))

lib/commonFileReader.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { FileSource, UploadInput } from './options.js'
22
import { ArrayBufferViewFileSource } from './sources/ArrayBufferViewFileSource.js'
33
import { BlobFileSource } from './sources/BlobFileSource.js'
4-
import { WebStreamFileSource, isWebStream } from './sources/WebStreamFileSource.js'
4+
import { WebStreamFileSource } from './sources/WebStreamFileSource.js'
55

66
/**
77
* openFile provides FileSources for input types that have to be handled in all environments,
@@ -33,7 +33,7 @@ export function openFile(input: UploadInput, chunkSize: number): FileSource | nu
3333
return new ArrayBufferViewFileSource(view)
3434
}
3535

36-
if (isWebStream(input)) {
36+
if (input instanceof ReadableStream) {
3737
chunkSize = Number(chunkSize)
3838
if (!Number.isFinite(chunkSize)) {
3939
throw new Error(
@@ -53,5 +53,5 @@ export const supportedTypes = [
5353
'ArrayBuffer',
5454
'SharedArrayBuffer',
5555
'ArrayBufferView',
56-
'ReadableStreamDefaultReader (Web Streams)',
56+
'ReadableStream (Web Streams)',
5757
]

lib/options.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ export type UploadInput =
4141
| ArrayBuffer
4242
| SharedArrayBuffer
4343
| ArrayBufferView // includes Node.js' Buffer
44-
// TODO: We should accept a ReadableStream instead of a reader
45-
| Pick<ReadableStreamDefaultReader, 'read'>
44+
| ReadableStream // Web Streams
4645
// available in Node.js
4746
| NodeReadableStream
4847
| PathReference

lib/sources/WebStreamFileSource.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { FileSource, SliceResult, UploadInput } from '../options.js'
1+
import type { FileSource, SliceResult } from '../options.js'
22

33
function len(blobOrArray: WebStreamFileSource['_buffer']): number {
44
if (blobOrArray === undefined) return 0
@@ -26,8 +26,9 @@ function concat<T extends WebStreamFileSource['_buffer']>(a: T, b: T): T {
2626
/**
2727
* WebStreamFileSource implements FileSource for Web Streams.
2828
*/
29+
// TODO: Can we share code with NodeStreamFileSource?
2930
export class WebStreamFileSource implements FileSource {
30-
private _reader: Pick<ReadableStreamDefaultReader<WebStreamFileSource['_buffer']>, 'read'>
31+
private _reader: ReadableStreamDefaultReader<Uint8Array>
3132

3233
private _buffer: Blob | Uint8Array | undefined
3334

@@ -43,8 +44,14 @@ export class WebStreamFileSource implements FileSource {
4344
// it manually (see the `uploadSize` option).
4445
size = null
4546

46-
constructor(reader: Pick<ReadableStreamDefaultReader, 'read'>) {
47-
this._reader = reader
47+
constructor(stream: ReadableStream) {
48+
if (stream.locked) {
49+
throw new Error(
50+
'Readable stream is already locked to reader. tus-js-client cannot obtain a new reader.',
51+
)
52+
}
53+
54+
this._reader = stream.getReader()
4855
}
4956

5057
async slice(start: number, end: number): Promise<SliceResult> {
@@ -102,14 +109,6 @@ export class WebStreamFileSource implements FileSource {
102109
}
103110

104111
close() {
105-
// TODO: We should not call cancel
106-
//@ts-expect-error cancel is not defined since we only pick `read`
107-
if (this._reader.cancel) this._reader.cancel()
112+
this._reader.cancel()
108113
}
109114
}
110-
111-
export function isWebStream(
112-
input: UploadInput,
113-
): input is Pick<ReadableStreamDefaultReader, 'read'> {
114-
return 'read' in input && typeof input.read === 'function'
115-
}

test/spec/test-web-stream.js

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,20 @@ import { TestHttpStack, waitableFunction } from './helpers/utils.js'
33

44
describe('tus', () => {
55
describe('#Upload', () => {
6-
describe('uploading data from a Web Stream reader', () => {
6+
describe('uploading data from a Web Stream ReadableStream', () => {
77
function makeReader(content, readSize = content.length) {
8-
const reader = {
9-
value: new TextEncoder().encode(content),
10-
read() {
11-
let value
12-
let done = false
13-
if (this.value.length > 0) {
14-
value = this.value.subarray(0, readSize)
15-
this.value = this.value.subarray(readSize)
8+
let remainingData = new TextEncoder().encode(content)
9+
return new ReadableStream({
10+
pull(controller) {
11+
if (remainingData.length > 0) {
12+
const chunk = remainingData.subarray(0, readSize)
13+
remainingData = remainingData.subarray(readSize)
14+
controller.enqueue(chunk)
1615
} else {
17-
done = true
16+
controller.close()
1817
}
19-
return Promise.resolve({ value, done })
2018
},
21-
cancel: waitableFunction('cancel'),
22-
}
23-
24-
return reader
19+
})
2520
}
2621

2722
async function assertReaderUpload({ readSize, chunkSize }) {

0 commit comments

Comments
 (0)