Making good progress on WebCodecs.

This commit is contained in:
Luke Curley 2023-03-28 07:54:41 +09:00
parent cc00a79881
commit 805f6ca392
22 changed files with 1159 additions and 5120 deletions

4235
player/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,8 @@
}, },
"devDependencies": { "devDependencies": {
"@parcel/validator-typescript": "^2.6.0", "@parcel/validator-typescript": "^2.6.0",
"parcel": "^2.6.0", "parcel": "^2.8.0",
"typescript": ">=3.0.0" "typescript": ">=3.0.0",
"@types/dom-webcodecs": "^0.1.6"
} }
} }

120
player/src/audio/decoder.ts Normal file
View File

@ -0,0 +1,120 @@
import * as Message from "./message";
import { InitParser } from "../mp4/init";
import { Renderer } from "./renderer"
import { MP4New, MP4Sample, MP4ArrayBuffer } from "../mp4/index"
export class Decoder {
tracks: Map<string, InitParser>;
renderer: Renderer;
constructor(renderer: Renderer) {
this.tracks = new Map();
this.renderer = renderer;
}
async init(msg: Message.Init) {
let track = this.tracks.get(msg.track);
if (!track) {
track = new InitParser()
this.tracks.set(msg.track, track)
}
while (1) {
const data = await msg.stream.read()
if (!data) break
track.init(data)
}
// TODO this will hang on incomplete data
const init = await track.ready;
const info = init.info;
if (info.audioTracks.length != 1 || info.videoTracks.length != 0) {
throw new Error("expected a single audio track")
}
}
async decode(msg: Message.Segment) {
let track = this.tracks.get(msg.track);
if (!track) {
track = new InitParser()
this.tracks.set(msg.track, track)
}
// Wait for the init segment to be fully received and parsed
const init = await track.ready;
const info = init.info;
const video = info.videoTracks[0]
const decoder = new AudioDecoder({
output: (frame: AudioFrame) => {
this.renderer.push(frame)
},
error: (err: Error) => {
console.warn(err)
}
});
decoder.configure({
codec: info.mime,
// TODO what else?
// optimizeForLatency: true
})
const input = MP4New();
input.onSamples = (id: number, user: any, samples: MP4Sample[]) => {
for (let sample of samples) {
const timestamp = sample.dts / (1000 / info.timescale) // milliseconds
decoder.decode(new EncodedAudioChunk({
data: sample.data,
duration: sample.duration,
timestamp: timestamp,
}))
}
}
input.onReady = (info: any) => {
input.setExtractionOptions(info.tracks[0].id, {}, { nbSamples: 1 });
input.start();
}
let offset = 0
// MP4box requires us to reparse the init segment unfortunately
for (let raw of track.raw) {
offset = input.appendBuffer(raw)
}
/* TODO I'm not actually sure why this code doesn't work; something trips up the MP4 parser
while (1) {
const data = await stream.read()
if (!data) break
input.appendBuffer(data)
input.flush()
}
*/
// One day I'll figure it out; until then read one top-level atom at a time
while (!await msg.stream.done()) {
const raw = await msg.stream.peek(4)
const size = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint32(0)
const atom = await msg.stream.bytes(size)
// Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately
let box = new Uint8Array(atom.byteLength);
box.set(atom)
// and for some reason we need to modify the underlying ArrayBuffer with offset
let buffer = box.buffer as MP4ArrayBuffer
buffer.fileStart = offset
// Parse the data
offset = input.appendBuffer(buffer)
input.flush()
}
}
}

View File

@ -1,3 +1,19 @@
self.addEventListener('message', (e: Event) => { import * as Message from "./message"
}) // Wrapper around the WebWorker API
export default class Audio {
worker: Worker;
constructor(config: Message.Config) {
this.worker = new Worker(new URL('worker.ts', import.meta.url), { type: "module" })
this.worker.postMessage({ config }, [ ])
}
init(init: Message.Init) {
this.worker.postMessage({ init }, [ init.stream.buffer, init.stream.reader ])
}
segment(segment: Message.Segment) {
this.worker.postMessage({ segment }, [ segment.stream.buffer, segment.stream.reader ])
}
}

View File

@ -0,0 +1,15 @@
import Reader from "../stream/reader";
export interface Config {
canvas: OffscreenCanvas;
}
export interface Init {
track: string;
stream: Reader;
}
export interface Segment {
track: string;
stream: Reader;
}

View File

@ -0,0 +1,12 @@
import * as Message from "./message";
export class Renderer {
render: number; // non-zero if requestAnimationFrame has been called
sync: DOMHighResTimeStamp; // the wall clock value for timestamp 0
last?: number; // the timestamp of the last rendered frame
constructor(config: Message.Config) {
this.render = 0;
this.sync = 0;
}
}

View File

@ -0,0 +1,21 @@
import { Renderer } from "./renderer"
import { Decoder } from "./decoder"
import * as Message from "./message"
let decoder: Decoder;
let renderer: Renderer;
self.addEventListener('message', async (e: MessageEvent) => {
if (e.data.config) {
const config = e.data.config as Message.Config
renderer = new Renderer(config)
decoder = new Decoder(renderer)
}
if (e.data.segment) {
const segment = e.data.segment as Message.Segment
await decoder.decode(segment)
}
})

View File

@ -1,13 +1,13 @@
// Wrapper around MP4Box to play nicely with MP4Box. // Wrapper around MP4Box to play nicely with MP4Box.
// I tried getting a mp4box.all.d.ts file to work but just couldn't figure it out // I tried getting a mp4box.all.d.ts file to work but just couldn't figure it out
import { createFile, ISOFile, DataStream, BoxParser } from "./mp4box.all.js" import { createFile, ISOFile, DataStream, BoxParser } from "./mp4box.all"
// Rename some stuff so it's on brand. // Rename some stuff so it's on brand.
export { createFile as MP4New, ISOFile as MP4File, DataStream as MP4Stream, BoxParser as MP4Parser } export { createFile as New, ISOFile as File, DataStream as Stream, BoxParser as Parser }
export type MP4ArrayBuffer = ArrayBuffer & {fileStart: number}; export type ArrayBufferOffset = ArrayBuffer & {fileStart: number};
export interface MP4MediaTrack { export interface MediaTrack {
id: number; id: number;
created: Date; created: Date;
modified: Date; modified: Date;
@ -25,13 +25,13 @@ export interface MP4MediaTrack {
nb_samples: number; nb_samples: number;
} }
export interface MP4VideoData { export interface VideoData {
width: number; width: number;
height: number; height: number;
} }
export interface MP4VideoTrack extends MP4MediaTrack { export interface VideoTrack extends MediaTrack {
video: MP4VideoData; video: VideoData;
} }
export interface MP4AudioData { export interface MP4AudioData {
@ -40,13 +40,13 @@ export interface MP4AudioData {
sample_size: number; sample_size: number;
} }
export interface MP4AudioTrack extends MP4MediaTrack { export interface AudioTrack extends MediaTrack {
audio: MP4AudioData; audio: MP4AudioData;
} }
export type MP4Track = MP4VideoTrack | MP4AudioTrack; export type Track = VideoTrack | AudioTrack;
export interface MP4Info { export interface Info {
duration: number; duration: number;
timescale: number; timescale: number;
fragment_duration: number; fragment_duration: number;
@ -56,13 +56,13 @@ export interface MP4Info {
brands: string[]; brands: string[];
created: Date; created: Date;
modified: Date; modified: Date;
tracks: MP4Track[]; tracks: Track[];
mime: string; mime: string;
videoTracks: MP4Track[]; videoTracks: Track[];
audioTracks: MP4Track[]; audioTracks: Track[];
} }
export interface MP4Sample { export interface Sample {
number: number; number: number;
track_id: number; track_id: number;
timescale: number; timescale: number;
@ -83,3 +83,5 @@ export interface MP4Sample {
offset: number; offset: number;
subsamples: any; subsamples: any;
} }
export { Init, InitParser } from "./init"

View File

@ -1,24 +1,28 @@
import { MP4New, MP4File, MP4ArrayBuffer, MP4Info } from "../mp4/mp4" import * as MP4 from "./index"
export interface Init {
raw: MP4.ArrayBufferOffset;
info: MP4.Info;
}
export class InitParser { export class InitParser {
mp4box: MP4File; mp4box: MP4.File;
offset: number; offset: number;
raw: MP4ArrayBuffer[]; raw: MP4.ArrayBufferOffset[];
ready: Promise<Init>; info: Promise<MP4.Info>;
constructor() { constructor() {
this.mp4box = MP4New() this.mp4box = MP4.New()
this.raw = [] this.raw = []
this.offset = 0 this.offset = 0
// Create a promise that gets resolved once the init segment has been parsed. // Create a promise that gets resolved once the init segment has been parsed.
this.ready = new Promise((resolve, reject) => { this.info = new Promise((resolve, reject) => {
this.mp4box.onError = reject this.mp4box.onError = reject
// https://github.com/gpac/mp4box.js#onreadyinfo // https://github.com/gpac/mp4box.js#onreadyinfo
this.mp4box.onReady = (info: MP4Info) => { this.mp4box.onReady = (info: MP4.Info) => {
if (!info.isFragmented) { if (!info.isFragmented) {
reject("expected a fragmented mp4") reject("expected a fragmented mp4")
} }
@ -27,10 +31,7 @@ export class InitParser {
reject("expected a single track") reject("expected a single track")
} }
resolve({ resolve(info)
info: info,
raw: this.raw,
})
} }
}) })
} }
@ -41,7 +42,7 @@ export class InitParser {
box.set(data) box.set(data)
// and for some reason we need to modify the underlying ArrayBuffer with fileStart // and for some reason we need to modify the underlying ArrayBuffer with fileStart
let buffer = box.buffer as MP4ArrayBuffer let buffer = box.buffer as MP4.ArrayBufferOffset
buffer.fileStart = this.offset buffer.fileStart = this.offset
// Parse the data // Parse the data
@ -52,8 +53,3 @@ export class InitParser {
this.raw.push(buffer) this.raw.push(buffer)
} }
} }
export interface Init {
raw: MP4ArrayBuffer[];
info: MP4Info;
}

View File

@ -0,0 +1,2 @@
export { default as Reader } from "./reader"
export { default as Writer } from "./writer"

View File

@ -10,19 +10,26 @@ export default class Reader {
// Returns any number of bytes // Returns any number of bytes
async read(): Promise<Uint8Array | undefined> { async read(): Promise<Uint8Array | undefined> {
if (this.buffer.byteLength) { if (this.buffer.byteLength) {
const buffer = this.buffer; const buffer = this.buffer;
this.buffer = new Uint8Array() this.buffer = new Uint8Array()
return buffer return buffer
} }
const result = await this.reader.getReader().read() const r = this.reader.getReader()
const result = await r.read()
r.releaseLock()
return result.value return result.value
} }
async readAll(): Promise<Uint8Array> { async readAll(): Promise<Uint8Array> {
const r = this.reader.getReader()
while (1) { while (1) {
const result = await this.reader.getReader().read() const result = await r.read()
if (result.done) { if (result.done) {
break break
} }
@ -42,12 +49,16 @@ export default class Reader {
const result = this.buffer const result = this.buffer
this.buffer = new Uint8Array() this.buffer = new Uint8Array()
r.releaseLock()
return result return result
} }
async bytes(size: number): Promise<Uint8Array> { async bytes(size: number): Promise<Uint8Array> {
const r = this.reader.getReader()
while (this.buffer.byteLength < size) { while (this.buffer.byteLength < size) {
const result = await this.reader.getReader().read() const result = await r.read()
if (result.done) { if (result.done) {
throw "short buffer" throw "short buffer"
} }
@ -67,12 +78,16 @@ export default class Reader {
const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size)
this.buffer = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset + size) this.buffer = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset + size)
r.releaseLock()
return result return result
} }
async peek(size: number): Promise<Uint8Array> { async peek(size: number): Promise<Uint8Array> {
const r = this.reader.getReader()
while (this.buffer.byteLength < size) { while (this.buffer.byteLength < size) {
const result = await this.reader.getReader().read() const result = await r.read()
if (result.done) { if (result.done) {
throw "short buffer" throw "short buffer"
} }
@ -89,7 +104,11 @@ export default class Reader {
} }
} }
return new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size)
r.releaseLock()
return result
} }
async view(size: number): Promise<DataView> { async view(size: number): Promise<DataView> {

View File

@ -1,6 +1,7 @@
import * as Message from "./message" import * as Message from "./message"
import Reader from "../stream/reader" import * as Stream from "../stream"
import Writer from "../stream/writer" import * as MP4 from "../mp4"
import Video from "../video/index" import Video from "../video/index"
///<reference path="./types/webtransport.d.ts"/> ///<reference path="./types/webtransport.d.ts"/>
@ -13,11 +14,14 @@ export interface PlayerInit {
export class Player { export class Player {
quic: Promise<WebTransport>; quic: Promise<WebTransport>;
api: Promise<WritableStream>; api: Promise<WritableStream>;
tracks: Map<string, MP4.InitParser>
//audio: Worker; //audio: Worker;
video: Video; video: Video;
constructor(props: PlayerInit) { constructor(props: PlayerInit) {
this.tracks = new Map();
//this.audio = new Worker("../audio") //this.audio = new Worker("../audio")
this.video = new Video({ this.video = new Video({
canvas: props.canvas.transferControlToOffscreen(), canvas: props.canvas.transferControlToOffscreen(),
@ -76,7 +80,7 @@ export class Player {
const stream = await this.api const stream = await this.api
const writer = new Writer(stream) const writer = new Stream.Writer(stream)
await writer.uint32(size) await writer.uint32(size)
await writer.string("warp") await writer.string("warp")
await writer.string(payload) await writer.string(payload)
@ -97,7 +101,7 @@ export class Player {
} }
async handleStream(stream: ReadableStream) { async handleStream(stream: ReadableStream) {
let r = new Reader(stream.getReader()) let r = new Stream.Reader(stream)
while (!await r.done()) { while (!await r.done()) {
const size = await r.uint32(); const size = await r.uint32();
@ -117,19 +121,52 @@ export class Player {
} }
} }
async handleInit(stream: Reader, msg: Message.Init) { async handleInit(stream: Stream.Reader, msg: Message.Init) {
// TODO properly determine if audio or video let track = this.tracks.get(msg.id);
this.video.init({ if (!track) {
track: msg.id, track = new MP4.InitParser()
stream: stream, this.tracks.set(msg.id, track)
}) }
while (1) {
const data = await stream.read()
if (!data) break
track.push(data)
}
const info = await track.info
if (info.audioTracks.length + info.videoTracks.length != 1) {
throw new Error("expected a single track")
}
if (info.videoTracks.length) {
this.video.init({
track: msg.id,
info: info,
raw: track.raw,
})
}
} }
async handleSegment(stream: Reader, msg: Message.Segment) { async handleSegment(stream: Stream.Reader, msg: Message.Segment) {
// TODO properly determine if audio or video let track = this.tracks.get(msg.init);
this.video.segment({ if (!track) {
track: msg.init, track = new MP4.InitParser()
stream: stream, this.tracks.set(msg.init, track)
}) }
const info = await track.info
// Wait until we learn if this is an audio or video track
if (info.videoTracks.length) {
this.video.segment({
track: msg.init,
buffer: stream.buffer,
reader: stream.reader,
})
}
} }
} }

View File

@ -0,0 +1,16 @@
export default class Deferred<T> {
promise: Promise<T>
resolve: (value: T | PromiseLike<T>) => void
reject: (value: T | PromiseLike<T>) => void
constructor() {
// Set initial values so TS stops being annoying.
this.resolve = (value: T | PromiseLike<T>) => {};
this.reject = (value: T | PromiseLike<T>) => {};
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
}
}

1
player/src/util/index.ts Normal file
View File

@ -0,0 +1 @@
export { default as Deferred } from "./deferred"

View File

@ -1,10 +1,13 @@
import * as Message from "./message"; import * as Message from "./message";
import { Track } from "./track"; import * as MP4 from "../mp4"
import * as Stream from "../stream"
import * as Util from "../util"
import { Renderer } from "./renderer" import { Renderer } from "./renderer"
import { MP4New, MP4Sample, MP4ArrayBuffer } from "../mp4/mp4"
export class Decoder { export class Decoder {
tracks: Map<string, Track>; // Store the init message for each track
tracks: Map<string, Util.Deferred<Message.Init>>
renderer: Renderer; renderer: Renderer;
constructor(renderer: Renderer) { constructor(renderer: Renderer) {
@ -13,37 +16,24 @@ export class Decoder {
} }
async init(msg: Message.Init) { async init(msg: Message.Init) {
let track = this.tracks.get(msg.track); let track = this.tracks.get(msg.track);
if (!track) { if (!track) {
track = new Track() track = new Util.Deferred()
this.tracks.set(msg.track, track) this.tracks.set(msg.track, track)
} }
while (1) { track.resolve(msg)
const data = await msg.stream.read()
if (!data) break
track.init(data)
}
// TODO this will hang on incomplete data
const init = await track.ready;
const info = init.info;
if (info.audioTracks.length + info.videoTracks.length != 1) {
throw new Error("expected a single track")
}
} }
async decode(msg: Message.Segment) { async decode(msg: Message.Segment) {
let track = this.tracks.get(msg.track); let track = this.tracks.get(msg.track);
if (!track) { if (!track) {
track = new Track() track = new Util.Deferred()
this.tracks.set(msg.track, track) this.tracks.set(msg.track, track)
} }
// Wait for the init segment to be fully received and parsed // Wait for the init segment to be fully received and parsed
const init = await track.ready; const init = await track.promise;
const info = init.info; const info = init.info;
const video = info.videoTracks[0] const video = info.videoTracks[0]
@ -57,17 +47,18 @@ export class Decoder {
}); });
decoder.configure({ decoder.configure({
codec: info.mime, codec: video.codec,
codedHeight: video.track_height, codedHeight: video.track_height,
codedWidth: video.track_width, codedWidth: video.track_width,
// optimizeForLatency: true // optimizeForLatency: true
}) })
const input = MP4New(); const input = MP4.New();
input.onSamples = (id: number, user: any, samples: MP4Sample[]) => { input.onSamples = (id: number, user: any, samples: MP4.Sample[]) => {
for (let sample of samples) { for (let sample of samples) {
const timestamp = sample.dts / (1000 / info.timescale) // milliseconds const timestamp = sample.dts / (1000 / info.timescale) // milliseconds
console.log(sample)
decoder.decode(new EncodedVideoChunk({ decoder.decode(new EncodedVideoChunk({
data: sample.data, data: sample.data,
@ -83,13 +74,16 @@ export class Decoder {
input.start(); input.start();
} }
let offset = 0
// MP4box requires us to reparse the init segment unfortunately // MP4box requires us to reparse the init segment unfortunately
for (let raw of track.raw) { let offset = 0;
offset = input.appendBuffer(raw)
for (let raw of init.raw) {
raw.fileStart = offset
input.appendBuffer(raw)
} }
const stream = new Stream.Reader(msg.reader, msg.buffer)
/* TODO I'm not actually sure why this code doesn't work; something trips up the MP4 parser /* TODO I'm not actually sure why this code doesn't work; something trips up the MP4 parser
while (1) { while (1) {
const data = await stream.read() const data = await stream.read()
@ -101,23 +95,22 @@ export class Decoder {
*/ */
// One day I'll figure it out; until then read one top-level atom at a time // One day I'll figure it out; until then read one top-level atom at a time
while (!await msg.stream.done()) { while (!await stream.done()) {
const raw = await msg.stream.peek(4) const raw = await stream.peek(4)
const size = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint32(0) const size = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint32(0)
const atom = await msg.stream.bytes(size) const atom = await stream.bytes(size)
// Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately // Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately
let box = new Uint8Array(atom.byteLength); let box = new Uint8Array(atom.byteLength);
box.set(atom) box.set(atom)
// and for some reason we need to modify the underlying ArrayBuffer with offset // and for some reason we need to modify the underlying ArrayBuffer with offset
let buffer = box.buffer as MP4ArrayBuffer let buffer = box.buffer as MP4.ArrayBufferOffset
buffer.fileStart = offset buffer.fileStart = offset
// Parse the data // Parse the data
offset = input.appendBuffer(buffer) offset = input.appendBuffer(buffer)
input.flush() input.flush()
} }
} }
} }

View File

@ -10,10 +10,10 @@ export default class Video {
} }
init(init: Message.Init) { init(init: Message.Init) {
this.worker.postMessage({ init }, [ init.stream.buffer, init.stream.reader ]) this.worker.postMessage({ init }) // note: we copy the raw init bytes each time
} }
segment(segment: Message.Segment) { segment(segment: Message.Segment) {
this.worker.postMessage({ segment }, [ segment.stream.buffer, segment.stream.reader ]) this.worker.postMessage({ segment }, [ segment.buffer.buffer, segment.reader ])
} }
} }

View File

@ -1,4 +1,4 @@
import Reader from "../stream/reader"; import * as MP4 from "../mp4"
export interface Config { export interface Config {
canvas: OffscreenCanvas; canvas: OffscreenCanvas;
@ -6,10 +6,12 @@ export interface Config {
export interface Init { export interface Init {
track: string; track: string;
stream: Reader; info: MP4.Info;
raw: MP4.ArrayBufferOffset[];
} }
export interface Segment { export interface Segment {
track: string; track: string;
stream: Reader; buffer: Uint8Array; // unread buffered data
reader: ReadableStream; // unread unbuffered data
} }

View File

@ -1,58 +0,0 @@
import { MP4New, MP4File, MP4ArrayBuffer, MP4Info } from "../mp4/mp4"
export interface Init {
raw: MP4ArrayBuffer[];
info: MP4Info;
}
export class Track {
mp4box: MP4File;
offset: number;
raw: MP4ArrayBuffer[];
ready: Promise<Init>;
constructor() {
this.mp4box = MP4New()
this.raw = []
this.offset = 0
// Create a promise that gets resolved once the init segment has been parsed.
this.ready = new Promise((resolve, reject) => {
this.mp4box.onError = reject
// https://github.com/gpac/mp4box.js#onreadyinfo
this.mp4box.onReady = (info: MP4Info) => {
if (!info.isFragmented) {
reject("expected a fragmented mp4")
}
if (info.tracks.length != 1) {
reject("expected a single track")
}
resolve({
raw: this.raw,
info,
})
}
})
}
init(data: Uint8Array) {
// Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately
let box = new Uint8Array(data.byteLength);
box.set(data)
// and for some reason we need to modify the underlying ArrayBuffer with fileStart
let buffer = box.buffer as MP4ArrayBuffer
buffer.fileStart = this.offset
// Parse the data
this.offset = this.mp4box.appendBuffer(buffer)
this.mp4box.flush()
// Add the box to our queue of chunks
this.raw.push(buffer)
}
}

View File

@ -11,11 +11,11 @@ self.addEventListener('message', async (e: MessageEvent) => {
renderer = new Renderer(config) renderer = new Renderer(config)
decoder = new Decoder(renderer) decoder = new Decoder(renderer)
} } else if (e.data.init) {
const init = e.data.init as Message.Init
if (e.data.segment) { await decoder.init(init)
} else if (e.data.segment) {
const segment = e.data.segment as Message.Segment const segment = e.data.segment as Message.Segment
await decoder.decode(segment) await decoder.decode(segment)
} }
}) })

View File

@ -4,6 +4,8 @@
], ],
"compilerOptions": { "compilerOptions": {
"target": "es2022", "target": "es2022",
"module": "es2022",
"moduleResolution": "node",
"strict": true, "strict": true,
"typeRoots": [ "typeRoots": [
"src/types" "src/types"

File diff suppressed because it is too large Load Diff

View File

@ -200,6 +200,11 @@ func (s *Session) writeInit(ctx context.Context, init *MediaInit) (err error) {
return fmt.Errorf("failed to write init data: %w", err) return fmt.Errorf("failed to write init data: %w", err)
} }
err = stream.Close()
if err != nil {
return fmt.Errorf("failed to close init stream: %w", err)
}
return nil return nil
} }
@ -210,6 +215,11 @@ func (s *Session) writeSegment(ctx context.Context, segment *MediaSegment) (err
return fmt.Errorf("failed to create stream: %w", err) return fmt.Errorf("failed to create stream: %w", err)
} }
if temp == nil {
// Not sure when this happens, perhaps when closing a connection?
return fmt.Errorf("received a nil stream from quic-go: %w", err)
}
// Wrap the stream in an object that buffers writes instead of blocking. // Wrap the stream in an object that buffers writes instead of blocking.
stream := NewStream(temp) stream := NewStream(temp)
s.streams.Add(stream.Run) s.streams.Add(stream.Run)