diff --git a/dev/demux.html b/dev/demux.html index ebe2944..d7a8a32 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -11,7 +11,6 @@ document.body.append(fileInput); fileInput.addEventListener('change', async () => { - /* const file = fileInput.files[0]; const input = new Mediabunny.Input({ formats: Mediabunny.ALL_FORMATS, @@ -19,11 +18,13 @@ }); const track = await input.getPrimaryAudioTrack(); + const sink3 = new Mediabunny.EncodedPacketSink(track); + console.log((await sink3.getFirstPacket()).data.join(', ')); + return; console.log(await track.getDurationFromMetadata(), await track.computeDuration()); //console.log(await input.getDurationFromMetadata(), await input.computeDuration()); return; - */ const manifest = new Mediabunny.Input({ entryPath: 'https://playertest.longtailvideo.com/adaptive/alt-audio-no-video/sintel/playlist.m3u8', diff --git a/src/hls/hls-muxer.ts b/src/hls/hls-muxer.ts index ec55751..796661e 100644 --- a/src/hls/hls-muxer.ts +++ b/src/hls/hls-muxer.ts @@ -1,6 +1,6 @@ import { MediaCodec, validateAudioChunkMetadata, validateVideoChunkMetadata } from '../codec'; -import { EncodedAudioPacketSource, EncodedVideoPacketSource, MediaSource } from '../media-source'; -import { assert, joinPaths, textEncoder, UNDETERMINED_LANGUAGE } from '../misc'; +import { EncodedAudioPacketSource, EncodedVideoPacketSource } from '../media-source'; +import { arrayArgmax, assert, findLastIndex, joinPaths, textEncoder, UNDETERMINED_LANGUAGE } from '../misc'; import { Muxer } from '../muxer'; import { Output, @@ -32,7 +32,6 @@ type HlsAudioTrackData = HlsTrackData & { info: { type: 'audio' } }; type Playlist = { id: number; - key: string; path: string; tracks: OutputTrack[]; segmentFormat: OutputFormat; @@ -349,6 +348,10 @@ export class HlsMuxer extends Muxer { }; const registerPlaylist = async (tracks: OutputTrack[]) => { + if (tracks.some(track => this.playlists.some(playlist => playlist.tracks.includes(track)))) { + throw new Error('Internal error: track is already registered in a playlist.'); // Should be unreachable + } + const id = this.playlists.length + 1; const path = await this.getPlaylistPath({ n: id, @@ -363,12 +366,10 @@ export class HlsMuxer extends Muxer { ); } - const key = tracks.map(x => x.id).join('-'); const format = deduceSegmentFormat(tracks); const playlist: Playlist = { id: this.playlists.length + 1, - key, path, tracks, segmentFormat: format, @@ -393,8 +394,7 @@ export class HlsMuxer extends Muxer { for (let i = 0; i < group.tracks.length; i++) { const track = group.tracks[i]!; - const key = track.id.toString(); - let playlist = this.playlists.find(x => x.key === key); + let playlist = this.playlists.find(x => x.tracks[0]!.id === track.id); playlist ??= await registerPlaylist([track]); this.playlistDeclarations.push({ @@ -407,8 +407,8 @@ export class HlsMuxer extends Muxer { } for (const variant of variantStreams) { - const key = variant.tracks.map(x => x.id).join('-'); - let playlist = this.playlists.find(x => x.key === key); + // Since tracks can only be assigned to one playlist, the first track's ID acts as a "playlist key" + let playlist = this.playlists.find(x => x.tracks[0]!.id === variant.tracks[0]!.id); playlist ??= await registerPlaylist(variant.tracks); this.playlistDeclarations.push({ @@ -423,7 +423,7 @@ export class HlsMuxer extends Muxer { } async getMimeType(): Promise { - throw new Error('TODO'); + return 'application/vnd.apple.mpegurl'; } private allTracksAreKnown(playlist: Playlist) { @@ -590,85 +590,131 @@ export class HlsMuxer extends Muxer { return; } + const trackDatas = this.trackDatas.filter(x => playlist.tracks.includes(x.track)); + const videoTrack = trackDatas.find(x => x.info.type === 'video') as HlsVideoTrackData | undefined; + const audioTrack = trackDatas.find(x => x.info.type === 'audio') as HlsAudioTrackData | undefined; + + const videoClosed = videoTrack + ? videoTrack.track.source._closed || isFinalCall + : true; + const audioClosed = audioTrack + ? audioTrack.track.source._closed || isFinalCall + : true; + + // Loop in case we can finalize multiple segments while (true) { const currentSegmentEndTimestamp = playlist.currentSegmentStartTimestamp + this.targetSegmentDuration; - let videoKeyEndTimestamp: number | null = null; - let videoEndTimestamp: number | null = null; - let audioEndTimestamp: number | null = null; - let flushAllVideo = false; - let flushAllAudio = false; + // These store the index (exclusive) until when packets can be added to the next segment + let videoEndIndex = 0; + let audioEndIndex = 0; - const trackDatas = this.trackDatas.filter(x => playlist.tracks.includes(x.track)); + if (videoTrack && (!videoClosed || videoTrack.packets.length > 0)) { + const allBelow = videoTrack.packets.every(x => x.timestamp < currentSegmentEndTimestamp); - for (const trackData of trackDatas) { - for (let i = 0; i < trackData.packets.length; i++) { - const packet = trackData.packets[i]!; - const endTimestamp = packet.timestamp + packet.duration; + let bestKeyPacket: EncodedPacket | null = null; + let bestKeyPacketIndex: number | null = null; - if (trackData.info.type === 'video') { - videoEndTimestamp = Math.max(videoEndTimestamp ?? -Infinity, endTimestamp); + if (allBelow) { + if (!videoClosed) { + // Not enough data yet + return; + } + } else { + // Find the best key packet timestamp + for (let i = 0; i < videoTrack.packets.length; i++) { + const packet = videoTrack.packets[i]!; - if (i > 0 && packet.type === 'key') { - if ( - videoKeyEndTimestamp !== null - && packet.timestamp > currentSegmentEndTimestamp - ) { - break; - } - - videoKeyEndTimestamp = packet.timestamp; - } - } else if (trackData.info.type === 'audio') { - if ( - audioEndTimestamp !== null - && packet.timestamp > currentSegmentEndTimestamp - ) { + if (bestKeyPacket !== null && packet.timestamp > currentSegmentEndTimestamp) { break; } - audioEndTimestamp = packet.timestamp; + if (i > 0 && packet.type === 'key') { + bestKeyPacket = packet; + bestKeyPacketIndex = i; + } + } + } - const endTimestamp = packet.timestamp + packet.duration; - if (endTimestamp <= currentSegmentEndTimestamp) { - audioEndTimestamp = Math.max(audioEndTimestamp ?? -Infinity, endTimestamp); - if (i === trackData.packets.length - 1) { - flushAllAudio = true; + if (bestKeyPacketIndex !== null) { + videoEndIndex = bestKeyPacketIndex; + + if (audioTrack) { + // The audio track must go at least until the video key frame + const index = audioTrack.packets.findIndex(x => x.timestamp >= bestKeyPacket!.timestamp); + if (index !== -1) { + audioEndIndex = index; + } else { + if (audioClosed) { + audioEndIndex = audioTrack.packets.length; + } else { + return; + } + } + } + } else { + if (!videoClosed) { + return; + } + + // Include the entire rest of the video (since there's no key frame to split it on) + videoEndIndex = videoTrack.packets.length; + const maxIndex = arrayArgmax(videoTrack.packets, x => x.timestamp); + const maxPacket = videoTrack.packets[maxIndex]; + assert(maxPacket); + + if (audioTrack) { + if (maxPacket.timestamp < currentSegmentEndTimestamp) { + // The audio must go until at least the start of the next segment + const index = audioTrack.packets.findIndex(x => x.timestamp >= currentSegmentEndTimestamp); + if (index !== -1) { + audioEndIndex = index; + } else { + if (audioClosed) { + audioEndIndex = audioTrack.packets.length; + } else { + return; + } + } + } else { + // The audio must go beyond the last video packet + const index = audioTrack.packets.findIndex(x => x.timestamp > maxPacket.timestamp); + if (index !== -1) { + audioEndIndex = index; + } else { + if (audioClosed) { + audioEndIndex = audioTrack.packets.length; + } else { + return; + } } } } } - } + } else if (audioTrack && (!audioClosed || audioTrack.packets.length > 0)) { + const allBelow = audioTrack.packets.every(x => x.timestamp < currentSegmentEndTimestamp); - let endTimestamp: number | null = null; - if (videoKeyEndTimestamp !== null && videoEndTimestamp! > currentSegmentEndTimestamp) { - endTimestamp = videoKeyEndTimestamp; - } else { - if (isFinalCall) { - endTimestamp = videoEndTimestamp; - flushAllVideo = true; - } - - if (audioEndTimestamp !== null) { - endTimestamp = Math.max(endTimestamp ?? -Infinity, audioEndTimestamp); - } - } - - if (endTimestamp === null) { - return; - } else { - if (!isFinalCall && endTimestamp < currentSegmentEndTimestamp) { - return; - } - - for (const trackData of trackDatas) { - const closed = trackData.track.source._closed || isFinalCall; - if (!closed && !trackData.packets.some(x => x.timestamp >= endTimestamp)) { + if (allBelow) { + if (audioClosed) { + audioEndIndex = audioTrack.packets.length; + } else { return; } + } else { + const index = findLastIndex(audioTrack.packets, x => x.timestamp <= currentSegmentEndTimestamp); + if (index !== -1) { + audioEndIndex = index; + } else { + audioEndIndex = 1; + } } } + if (videoEndIndex === 0 && audioEndIndex === 0) { + return; + } + + // We can finalize a new segment! Let's first get the path const relativeSegmentPath = await this.getSegmentPath({ n: playlist.nextSegmentId, format: playlist.segmentFormat, @@ -705,67 +751,49 @@ export class HlsMuxer extends Muxer { }, }); + let maxEndTimestamp = -Infinity; + try { - const packetSources = new Map(); + let videoSource: EncodedVideoPacketSource | null = null; + let audioSource: EncodedAudioPacketSource | null = null; - for (const trackData of trackDatas) { - if (trackData.packets.length === 0) { - continue; - } + if (videoTrack) { + // Always add the track, no matter if it has packets or not (maintains underlying IDs) + videoSource = new EncodedVideoPacketSource((videoTrack.track as OutputVideoTrack).source._codec); + output.addVideoTrack(videoSource, videoTrack.track.metadata); + } - if (trackData.info.type === 'video') { - const outputTrack = trackData.track as OutputVideoTrack; - const source = new EncodedVideoPacketSource(outputTrack.source._codec); - output.addVideoTrack(source, outputTrack.metadata); - - packetSources.set(trackData, source); - } else if (trackData.info.type === 'audio') { - const outputTrack = trackData.track as OutputAudioTrack; - const source = new EncodedAudioPacketSource(outputTrack.source._codec); - output.addAudioTrack(source, outputTrack.metadata); - - packetSources.set(trackData, source); - } + if (audioTrack) { + // Always add the track, no matter if it has packets or not (maintains underlying IDs) + audioSource = new EncodedAudioPacketSource((audioTrack.track as OutputAudioTrack).source._codec); + output.addAudioTrack(audioSource, audioTrack.track.metadata); } await output.start(); - for (const trackData of trackDatas) { - const source = packetSources.get(trackData); - if (!source) { - continue; + // Add all of the packets + + if (videoTrack) { + assert(videoSource); + const meta = { decoderConfig: videoTrack.info.decoderConfig }; + + for (let i = 0; i < videoEndIndex; i++) { + const packet = videoTrack.packets[i]!; + + await videoSource.add(packet, meta); + maxEndTimestamp = Math.max(maxEndTimestamp, packet.timestamp + packet.duration); } + } - if (trackData.info.type === 'video') { - const videoPacketSource = source as EncodedVideoPacketSource; - const meta = { decoderConfig: trackData.info.decoderConfig }; + if (audioTrack) { + assert(audioSource); + const meta = { decoderConfig: audioTrack.info.decoderConfig }; - while (trackData.packets.length > 0) { - const nextPacket = trackData.packets[0]!; - if (!flushAllVideo && nextPacket.timestamp >= endTimestamp) { - break; - } + for (let i = 0; i < audioEndIndex; i++) { + const packet = audioTrack.packets[i]!; - trackData.packets.shift(); - await videoPacketSource.add(nextPacket, meta); - } - - videoPacketSource.close(); - } else if (trackData.info.type === 'audio') { - const audioPacketSource = source as EncodedAudioPacketSource; - const meta = { decoderConfig: trackData.info.decoderConfig }; - - while (trackData.packets.length > 0) { - const nextPacket = trackData.packets[0]!; - if (!flushAllAudio && nextPacket.timestamp >= endTimestamp) { - break; - } - - trackData.packets.shift(); - await audioPacketSource.add(nextPacket, meta); - } - - audioPacketSource.close(); + await audioSource.add(packet, meta); + maxEndTimestamp = Math.max(maxEndTimestamp, packet.timestamp + packet.duration); } } @@ -775,13 +803,35 @@ export class HlsMuxer extends Muxer { throw e; } + if (videoEndIndex > 0) { + assert(videoTrack); + videoTrack.packets.splice(0, videoEndIndex); + } + if (audioEndIndex > 0) { + assert(audioTrack); + audioTrack.packets.splice(0, audioEndIndex); + } + + let minNextTimestamp = Infinity; + if (videoTrack && videoTrack.packets.length > 0) { + minNextTimestamp = videoTrack.packets[0]!.timestamp; + } + if (audioTrack && audioTrack.packets.length > 0) { + minNextTimestamp = Math.min(minNextTimestamp, audioTrack.packets[0]!.timestamp); + } + + const nextSegmentStartTimestamp = minNextTimestamp < Infinity + ? minNextTimestamp + : maxEndTimestamp; // Happens for the last segment for example + assert(Number.isFinite(nextSegmentStartTimestamp)); + playlist.writtenSegments.push({ path: relativeSegmentPath, - duration: endTimestamp - playlist.currentSegmentStartTimestamp, + duration: nextSegmentStartTimestamp - playlist.currentSegmentStartTimestamp, byteSize: segmentSize, }); - playlist.currentSegmentStartTimestamp = endTimestamp; + playlist.currentSegmentStartTimestamp = nextSegmentStartTimestamp; } } @@ -822,11 +872,11 @@ export class HlsMuxer extends Muxer { const playlistText = '#EXTM3U\n' + '#EXT-X-VERSION:3\n' + '#EXT-X-PLAYLIST-TYPE:VOD\n' - + `#EXT-X-TARGETDURATION:${+targetDuration.toPrecision(13)}\n` + + `#EXT-X-TARGETDURATION:${Math.ceil(targetDuration)}\n` // Must be a "decimal-integer" + '\n' + (playlist.writtenSegments .map(segment => ( - `#EXTINF:${+segment.duration.toPrecision(13)}\n` + `#EXTINF:${+segment.duration.toFixed(12)},\n` // Trailing comma mandated by spec + `${segment.path}\n` )) .join('')) @@ -914,7 +964,7 @@ export class HlsMuxer extends Muxer { } if (videoTrack.metadata.frameRate !== undefined) { - masterPlaylistText += `,FRAME-RATE=${videoTrack.metadata.frameRate}`; + masterPlaylistText += `,FRAME-RATE=${+videoTrack.metadata.frameRate.toFixed(16)}`; } } diff --git a/src/index.ts b/src/index.ts index e174d18..977f0e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ export { FlacOutputFormat, FlacOutputFormatOptions, HlsOutputFormat, + HlsOutputFormatOptions, IsobmffOutputFormat, IsobmffOutputFormatOptions, MkvOutputFormat, @@ -158,6 +159,7 @@ export { AdtsInputFormat, FlacInputFormat, IsobmffInputFormat, + HlsInputFormat, MatroskaInputFormat, Mp3InputFormat, Mp4InputFormat, @@ -167,8 +169,10 @@ export { WaveInputFormat, WebMInputFormat, ALL_FORMATS, + HLS_FORMATS, ADTS, FLAC, + HLS, MATROSKA, MP3, MP4, diff --git a/src/input-format.ts b/src/input-format.ts index 9a54dc3..4a4fc99 100644 --- a/src/input-format.ts +++ b/src/input-format.ts @@ -703,3 +703,5 @@ export const HLS = /* #__PURE__ */ new HlsInputFormat(); * @public */ export const ALL_FORMATS: InputFormat[] = [HLS, MP4, QTFF, MATROSKA, WEBM, WAVE, OGG, FLAC, MP3, ADTS, MPEG_TS]; + +export const HLS_FORMATS: InputFormat[] = [HLS, MP4, QTFF, MP3, ADTS, MPEG_TS]; diff --git a/src/media-source.ts b/src/media-source.ts index e47679b..39b136c 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -22,12 +22,14 @@ import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } import { assert, assertNever, + binarySearchLessOrEqual, CallSerializer, clamp, clearIntervalUnthrottled, isFirefox, last, promiseWithResolvers, + roundToDivisor, setInt24, setIntervalUnthrottled, setUint24, @@ -213,6 +215,16 @@ class VideoEncoderWrapper { private codedHeight: number | null = null; private resizeCanvas: HTMLCanvasElement | OffscreenCanvas | null = null; + // VideoEncoder converts everything to microseconds, so we need to do some bookkeeping to restore the original + // timing information + private preciseTimings: { + microsecondTimestamp: number; + timestamp: number; + duration: number; + timestampIsValid: boolean; + durationIsValid: boolean; + }[] = []; + private customEncoder: CustomVideoEncoder | null = null; private customEncoderCallSerializer = new CallSerializer(); private customEncoderQueueSize = 0; @@ -353,6 +365,38 @@ class VideoEncoderWrapper { const videoFrame = videoSample.toVideoFrame(); + const preciseTimingIndex = binarySearchLessOrEqual( + this.preciseTimings, + videoFrame.timestamp, + x => x.microsecondTimestamp, + ); + const existingEntry = preciseTimingIndex !== -1 + ? this.preciseTimings[preciseTimingIndex] + : null; + if (existingEntry && existingEntry.microsecondTimestamp === videoFrame.timestamp) { + if (existingEntry.timestamp !== videoSample.timestamp) { + // Mapping isn't unique, can't use the timestamp + existingEntry.timestampIsValid = false; + } + if (existingEntry.duration !== videoSample.duration) { + // Mapping isn't unique, can't use the duration + existingEntry.durationIsValid = false; + } + } else { + this.preciseTimings.splice(preciseTimingIndex + 1, 0, { + microsecondTimestamp: videoFrame.timestamp, + timestamp: videoSample.timestamp, + duration: videoSample.duration, + timestampIsValid: true, + durationIsValid: true, + }); + + // Make sure it doesn't grow indefinitely + if (this.preciseTimings.length > 128) { + this.preciseTimings.shift(); + } + } + if (!this.alphaEncoder) { // No alpha encoder, simple case this.encoder.encode(videoFrame, finalEncodeOptions); @@ -515,7 +559,25 @@ class VideoEncoderWrapper { sideData.alpha = alphaData; } - const packet = EncodedPacket.fromEncodedChunk(colorChunk, sideData); + let packet = EncodedPacket.fromEncodedChunk(colorChunk, sideData); + + const preciseTimingIndex = binarySearchLessOrEqual( + this.preciseTimings, + colorChunk.timestamp, + x => x.microsecondTimestamp, + ); + const entry = preciseTimingIndex !== -1 + ? this.preciseTimings[preciseTimingIndex] + : null; + + // If there's a relevant timing entry, refine the packet's timing data to get better accuracy than + // microseconds + if (entry && entry.microsecondTimestamp === colorChunk.timestamp) { + packet = packet.clone({ + timestamp: entry.timestampIsValid ? entry.timestamp : undefined, + duration: entry.durationIsValid ? entry.duration : undefined, + }); + } this.encodingConfig.onEncodedPacket?.(packet, meta); void this.muxer!.addEncodedVideoPacket(this.source._connectedTrack!, packet, meta) @@ -1766,7 +1828,16 @@ class AudioEncoderWrapper { } } - const packet = EncodedPacket.fromEncodedChunk(chunk); + let packet = EncodedPacket.fromEncodedChunk(chunk); + + // Snap the timing information to the sample rate to recover information lost in microsecond + // conversion + packet = packet.clone({ + timestamp: roundToDivisor(packet.timestamp, encoderConfig.sampleRate), + duration: chunk.duration != null + ? roundToDivisor(packet.duration, encoderConfig.sampleRate) + : undefined, + }); this.encodingConfig.onEncodedPacket?.(packet, meta); void this.muxer!.addEncodedAudioPacket(this.source._connectedTrack!, packet, meta) diff --git a/src/misc.ts b/src/misc.ts index 0aa8fb0..ba82b56 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -791,6 +791,14 @@ export const joinPaths = (basePath: string, relativePath: string) => { return relativePath; } + // Strip query parameters from URL base paths so their contents don't mess up the join + if (basePath.includes('://')) { + const queryIndex = basePath.indexOf('?'); + if (queryIndex !== -1) { + basePath = basePath.slice(0, queryIndex); + } + } + let result: string; if (relativePath.startsWith('/')) { diff --git a/src/output-format.ts b/src/output-format.ts index 15ade17..6e5541b 100644 --- a/src/output-format.ts +++ b/src/output-format.ts @@ -1189,6 +1189,11 @@ export class HlsOutputFormat extends OutputFormat { get supportsTimestampedMediaData(): boolean { return true; // I guess?? } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + override _codecUnsupportedHint(codec: MediaCodec): string { + return ` Using different segment formats may grant support for this codec.`; + } } export const HLS_OUTPUT_FORMATS_DEFAULT = [new AdtsOutputFormat(), new MpegTsOutputFormat()]; diff --git a/test/node/hls-output.test.ts b/test/node/hls-output.test.ts index 8339206..70acd07 100644 --- a/test/node/hls-output.test.ts +++ b/test/node/hls-output.test.ts @@ -1,10 +1,17 @@ import { expect, test, vi } from 'vitest'; import { Output, OutputTrackGroup } from '../../src/output.js'; -import { HLS_OUTPUT_FORMATS_DEFAULT, HlsOutputFormat } from '../../src/output-format.js'; -import { NullTarget } from '../../src/target.js'; +import { HLS_OUTPUT_FORMATS_DEFAULT, HlsOutputFormat, MpegTsOutputFormat } from '../../src/output-format.js'; +import { BufferTarget, NullTarget } from '../../src/target.js'; import { EncodedAudioPacketSource, EncodedVideoPacketSource } from '../../src/media-source.js'; import { HlsMuxer } from '../../src/hls/hls-muxer.js'; import { AudioCodec, VideoCodec } from '../../src/codec.js'; +import { EncodedPacket, PacketType } from '../../src/packet.js'; +import { promiseWithResolvers } from '../../src/misc.js'; +import { Input } from '../../src/input.js'; +import { BufferSource } from '../../src/source.js'; +import { ALL_FORMATS } from '../../src/input-format.js'; +import { InputAudioTrack, InputVideoTrack } from '../../src/input-track.js'; +import { EncodedPacketSink } from '../../src/media-sink.js'; const videoSource = (codec: VideoCodec = 'avc') => new EncodedVideoPacketSource(codec); const audioSource = (codec: AudioCodec = 'aac') => new EncodedAudioPacketSource(codec); @@ -791,3 +798,936 @@ test('Playlist assignment, audio paired with audio', async () => { expect(consoleSpy.mock.calls).toHaveLength(1); expect(consoleSpy.mock.calls[0]![0]).toContain('Illegal pairing'); }); + +// eslint-disable-next-line @stylistic/max-len +const avcPacketData = new Uint8Array([0, 0, 0, 1, 9, 240, 0, 0, 0, 1, 39, 77, 64, 41, 169, 24, 15, 0, 68, 252, 184, 3, 80, 16, 16, 27, 108, 43, 94, 247, 192, 64, 0, 0, 0, 1, 40, 222, 9, 200, 0, 0, 1, 6, 0, 7, 128, 175, 200, 0, 0, 3, 0, 64, 128, 0, 0, 1, 6, 5, 17, 3, 135, 244, 78, 205, 10, 75, 220, 161, 148, 58, 195, 212, 155, 23, 31, 0, 128, 0, 0, 1, 37, 184, 32, 32, 33, 68, 197, 0, 1, 87, 155, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 175, 0, 0, 1, 37, 0, 127, 174, 8, 8, 8, 81, 49, 64, 0, 85, 230, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 147, 20, 0, 5, 94, 111, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 250, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 188, 0, 0, 1, 37, 0, 63, 203, 130, 2, 2, 20, 76, 80, 0, 21, 121, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 240, 0, 0, 1, 37, 0, 23, 234, 224, 128, 128, 133, 19, 20, 0, 5, 94, 111, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 249, 49, 64, 0, 85, 230, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 192, 0, 0, 1, 37, 0, 31, 226, 224, 128, 128, 133, 19, 20, 0, 5, 94, 111, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 250, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 188, 0, 0, 1, 37, 0, 9, 246, 184, 32, 32, 33, 68, 197, 0, 1, 87, 155, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 76, 80, 0, 21, 121, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 240, 0, 0, 1, 37, 0, 11, 244, 184, 32, 32, 33, 68, 197, 0, 1, 87, 155, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 175, 0, 0, 1, 37, 0, 13, 242, 184, 32, 32, 33, 68, 197, 0, 1, 87, 155, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 76, 80, 0, 21, 121, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 239, 190, 251, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 235, 174, 186, 255, 248, 255, 4, 17, 64, 0, 69, 19, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 247, 223, 125, 248]); ; +const avcMetadata: EncodedVideoChunkMetadata = { + decoderConfig: { + codec: 'avc1.4d401e', + codedWidth: 1280, + codedHeight: 720, + }, +}; + +// eslint-disable-next-line @stylistic/max-len +const aacPacketData = new Uint8Array([255, 241, 77, 128, 3, 159, 252, 0, 208, 0, 1, 3, 64, 0, 13, 0, 0, 17, 52, 0, 0, 208, 0, 3, 6, 128, 0, 56]); +const aacMetadata: EncodedAudioChunkMetadata = { + decoderConfig: { + codec: 'mp4a.40.2', + numberOfChannels: 2, + sampleRate: 48000, + }, +}; + +const setUpSegmentationEnvironment = async (options: { + video?: boolean; + audio?: boolean; +} = {}) => { + let result: string | null = null; + let segmentCount = 0; + let lastSegmentVideoTimestamps: Promise = Promise.resolve([]); + let lastSegmentAudioTimestamps: Promise = Promise.resolve([]); + + const output = new Output({ + format: new HlsOutputFormat({ + segmentFormats: [new MpegTsOutputFormat()], // No ADTS for simplicity + }), + target: (request) => { + const target = new BufferTarget(); + if (request.path.includes('playlist')) { + target.onfinalized = () => { + result = new TextDecoder().decode(target.buffer!); + }; + } else if (request.path.includes('segment')) { + segmentCount++; + + const videoBundle = promiseWithResolvers(); + lastSegmentVideoTimestamps = videoBundle.promise; + const audioBundle = promiseWithResolvers(); + lastSegmentAudioTimestamps = audioBundle.promise; + + target.onfinalized = async () => { + try { + using input = new Input({ + source: new BufferSource(target.buffer!), + formats: ALL_FORMATS, + }); + + const videoTrack = await input.getPrimaryVideoTrack() as InputVideoTrack; + if (videoTrack) { + const sink = new EncodedPacketSink(videoTrack); + const timestamps: number[] = []; + for await (const packet of sink.packets()) { + timestamps.push(packet.timestamp); + } + videoBundle.resolve(timestamps); + } else { + videoBundle.resolve([]); + } + + const audioTrack = await input.getPrimaryAudioTrack() as InputAudioTrack; + if (audioTrack) { + const sink = new EncodedPacketSink(audioTrack); + const timestamps: number[] = []; + for await (const packet of sink.packets()) { + timestamps.push(packet.timestamp); + } + audioBundle.resolve(timestamps); + } else { + audioBundle.resolve([]); + } + } catch { + videoBundle.resolve([]); + audioBundle.resolve([]); + } + }; + } + + return target; + }, + rootPath: '', + }); + + const _videoSource = options.video ? new EncodedVideoPacketSource('avc') : null; + const _audioSource = options.audio ? new EncodedAudioPacketSource('aac') : null; + + if (_videoSource) { + output.addVideoTrack(_videoSource); + } + if (_audioSource) { + output.addAudioTrack(_audioSource); + } + + await output.start(); + + const addVideoPacket = _videoSource + ? (timestamp: number, type: PacketType, duration = 0) => { + return _videoSource.add( + new EncodedPacket(avcPacketData, type, timestamp, duration), + avcMetadata, + ); + } + : null; + + const addAudioPacket = _audioSource + ? (timestamp: number, duration = 0) => { + return _audioSource.add( + new EncodedPacket(aacPacketData, 'key', timestamp, duration), + aacMetadata, + ); + } + : null; + + return { + output, + videoSource: _videoSource, + audioSource: _audioSource, + addVideoPacket, + addAudioPacket, + get segmentCount() { return segmentCount; }, + get result() { return result; }, + get lastSegmentVideoTimestamps() { return lastSegmentVideoTimestamps; }, + get lastSegmentAudioTimestamps() { return lastSegmentAudioTimestamps; }, + }; +}; + +test('Segmentation, simple', async () => { + const env = await setUpSegmentationEnvironment({ video: true }); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.5, 'delta'); + await env.addVideoPacket!(1, 'delta'); + await env.addVideoPacket!(1.5, 'delta'); + expect(env.segmentCount).toBe(0); + await env.addVideoPacket!(2, 'key'); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1, 1.5]); + await env.addVideoPacket!(2.5, 'delta'); + await env.addVideoPacket!(3, 'delta'); + await env.addVideoPacket!(3.5, 'delta'); + + await env.output.finalize(); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([2, 2.5, 3, 3.5]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:2, +segment-1-1.ts +#EXTINF:1.5, +segment-1-2.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, reaching until end of second segment', async () => { + const env = await setUpSegmentationEnvironment({ video: true }); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.5, 'delta'); + await env.addVideoPacket!(1, 'delta'); + await env.addVideoPacket!(1.5, 'delta'); + expect(env.segmentCount).toBe(0); + await env.addVideoPacket!(2, 'key'); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1, 1.5]); + await env.addVideoPacket!(2.5, 'delta'); + await env.addVideoPacket!(3, 'delta'); + await env.addVideoPacket!(3.5, 'delta'); + await env.addVideoPacket!(4, 'delta'); + expect(env.segmentCount).toBe(1); + + await env.output.finalize(); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([2, 2.5, 3, 3.5, 4]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:2, +segment-1-1.ts +#EXTINF:2, +segment-1-2.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, reaching until end of second segment with a final key packet', async () => { + const env = await setUpSegmentationEnvironment({ video: true }); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.5, 'delta'); + await env.addVideoPacket!(1, 'delta'); + await env.addVideoPacket!(1.5, 'delta'); + expect(env.segmentCount).toBe(0); + await env.addVideoPacket!(2, 'key'); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1, 1.5]); + await env.addVideoPacket!(2.5, 'delta'); + await env.addVideoPacket!(3, 'delta'); + await env.addVideoPacket!(3.5, 'delta'); + expect(env.segmentCount).toBe(1); + await env.addVideoPacket!(4, 'key'); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([2, 2.5, 3, 3.5]); + + await env.output.finalize(); + expect(env.segmentCount).toBe(3); + expect(await env.lastSegmentVideoTimestamps).toEqual([4]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:2, +segment-1-1.ts +#EXTINF:2, +segment-1-2.ts +#EXTINF:0, +segment-1-3.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, reaching until end of second segment with a final key packet (audio)', async () => { + const env = await setUpSegmentationEnvironment({ audio: true }); + + await env.addAudioPacket!(0); + await env.addAudioPacket!(0.5); + await env.addAudioPacket!(1); + await env.addAudioPacket!(1.5); + expect(env.segmentCount).toBe(0); + await env.addAudioPacket!(2); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentAudioTimestamps).toEqual([0, 0.5, 1, 1.5]); + + await env.output.finalize(); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentAudioTimestamps).toEqual([2]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:2, +segment-1-1.ts +#EXTINF:0, +segment-1-2.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, reaching until end of second segment with packet durations', async () => { + const env = await setUpSegmentationEnvironment({ video: true }); + + await env.addVideoPacket!(0, 'key', 0.5); + await env.addVideoPacket!(0.5, 'delta', 0.5); + await env.addVideoPacket!(1, 'delta', 0.5); + await env.addVideoPacket!(1.5, 'delta', 0.5); + expect(env.segmentCount).toBe(0); + await env.addVideoPacket!(2, 'key', 0.5); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1, 1.5]); + await env.addVideoPacket!(2.5, 'delta', 0.5); + await env.addVideoPacket!(3, 'delta', 0.5); + await env.addVideoPacket!(3.5, 'delta', 0.5); + expect(env.segmentCount).toBe(1); + await env.addVideoPacket!(4, 'key', 0.5); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([2, 2.5, 3, 3.5]); + + await env.output.finalize(); + expect(env.segmentCount).toBe(3); + expect(await env.lastSegmentVideoTimestamps).toEqual([4]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:2, +segment-1-1.ts +#EXTINF:2, +segment-1-2.ts +#EXTINF:0.5, +segment-1-3.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, reaching until end of second segment with packet durations (audio)', async () => { + const env = await setUpSegmentationEnvironment({ audio: true }); + + await env.addAudioPacket!(0, 0.5); + await env.addAudioPacket!(0.5, 0.5); + await env.addAudioPacket!(1, 0.5); + await env.addAudioPacket!(1.5, 0.5); + expect(env.segmentCount).toBe(0); + await env.addAudioPacket!(2, 0.5); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentAudioTimestamps).toEqual([0, 0.5, 1, 1.5]); + await env.addAudioPacket!(2.5, 0.5); + await env.addAudioPacket!(3, 0.5); + await env.addAudioPacket!(3.5, 0.5); + expect(env.segmentCount).toBe(1); + await env.addAudioPacket!(4, 0.5); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentAudioTimestamps).toEqual([2, 2.5, 3, 3.5]); + + await env.output.finalize(); + expect(env.segmentCount).toBe(3); + expect(await env.lastSegmentAudioTimestamps).toEqual([4]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:2, +segment-1-1.ts +#EXTINF:2, +segment-1-2.ts +#EXTINF:0.5, +segment-1-3.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, only one key packet', async () => { + const env = await setUpSegmentationEnvironment({ video: true }); + + await env.addVideoPacket!(0, 'key', 0.5); + await env.addVideoPacket!(0.5, 'delta', 0.5); + await env.addVideoPacket!(1, 'delta', 0.5); + await env.addVideoPacket!(1.5, 'delta', 0.5); + await env.addVideoPacket!(2, 'delta', 0.5); + await env.addVideoPacket!(2.5, 'delta', 0.5); + await env.addVideoPacket!(3, 'delta', 0.5); + expect(env.segmentCount).toBe(0); + + await env.output.finalize(); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:4 + +#EXTINF:3.5, +segment-1-1.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, key packets before the end of a segment (maximized segment duration test)', async () => { + const env = await setUpSegmentationEnvironment({ video: true }); + + await env.addVideoPacket!(0, 'key', 0.5); + await env.addVideoPacket!(0.5, 'delta', 0.5); + await env.addVideoPacket!(1, 'delta', 0.5); + await env.addVideoPacket!(1.5, 'key', 0.5); + expect(env.segmentCount).toBe(0); + await env.addVideoPacket!(2, 'delta', 0.5); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1]); + await env.addVideoPacket!(2.5, 'delta', 0.5); + await env.addVideoPacket!(3, 'key', 0.5); + expect(env.segmentCount).toBe(1); + await env.addVideoPacket!(3.5, 'delta', 0.5); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([1.5, 2, 2.5]); + await env.addVideoPacket!(4, 'delta', 0.5); + await env.addVideoPacket!(4.5, 'key', 0.5); + expect(env.segmentCount).toBe(2); + await env.addVideoPacket!(5, 'key', 0.5); + expect(env.segmentCount).toBe(3); + expect(await env.lastSegmentVideoTimestamps).toEqual([3, 3.5, 4, 4.5]); + + await env.output.finalize(); + expect(env.segmentCount).toBe(4); + expect(await env.lastSegmentVideoTimestamps).toEqual([5]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:1.5, +segment-1-1.ts +#EXTINF:1.5, +segment-1-2.ts +#EXTINF:2, +segment-1-3.ts +#EXTINF:0.5, +segment-1-4.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, full segment duration recovery after shorter segment', async () => { + const env = await setUpSegmentationEnvironment({ video: true }); + + await env.addVideoPacket!(0, 'key', 0.5); + await env.addVideoPacket!(0.5, 'delta', 0.5); + await env.addVideoPacket!(1, 'delta', 0.5); + await env.addVideoPacket!(1.5, 'key', 0.5); + expect(env.segmentCount).toBe(0); + await env.addVideoPacket!(2, 'delta', 0.5); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1]); + await env.addVideoPacket!(2.5, 'delta', 0.5); + await env.addVideoPacket!(3, 'delta', 0.5); + expect(env.segmentCount).toBe(1); + await env.addVideoPacket!(3.5, 'key', 0.5); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([1.5, 2, 2.5, 3]); + await env.addVideoPacket!(4, 'delta', 0.5); + + await env.output.finalize(); + expect(env.segmentCount).toBe(3); + expect(await env.lastSegmentVideoTimestamps).toEqual([3.5, 4]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:1.5, +segment-1-1.ts +#EXTINF:2, +segment-1-2.ts +#EXTINF:1, +segment-1-3.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, packet start timestamp intersecting with end timestamp of previous packet', async () => { + const env = await setUpSegmentationEnvironment({ video: true }); + + await env.addVideoPacket!(0, 'key', 0.5); + await env.addVideoPacket!(0.5, 'delta', 0.5); + await env.addVideoPacket!(1, 'delta', 0.5); + await env.addVideoPacket!(1.5, 'delta', 0.75); // This! + expect(env.segmentCount).toBe(0); + await env.addVideoPacket!(2, 'key', 0.5); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1, 1.5]); + + await env.output.finalize(); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([2]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:2, +segment-1-1.ts +#EXTINF:0.5, +segment-1-2.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, last video packet is included', async () => { + const env = await setUpSegmentationEnvironment({ video: true }); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.5, 'delta'); + await env.addVideoPacket!(1, 'delta'); + await env.addVideoPacket!(1.5, 'delta'); + expect(env.segmentCount).toBe(0); + + await env.output.finalize(); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1, 1.5]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:1.5, +segment-1-1.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, video not lining up with segment boundaries', async () => { + const env = await setUpSegmentationEnvironment({ video: true }); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.45, 'key'); + await env.addVideoPacket!(0.9, 'key'); + await env.addVideoPacket!(1.35, 'key'); + await env.addVideoPacket!(1.8, 'key'); + expect(env.segmentCount).toBe(0); + await env.addVideoPacket!(2.25, 'key'); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.45, 0.9, 1.35]); + await env.addVideoPacket!(2.7, 'key'); + await env.addVideoPacket!(3.15, 'key'); + await env.addVideoPacket!(3.6, 'key'); + expect(env.segmentCount).toBe(1); + await env.addVideoPacket!(4.05, 'key'); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([1.8, 2.25, 2.7, 3.15]); + + await env.output.finalize(); + expect(env.segmentCount).toBe(3); + console.log(await env.lastSegmentVideoTimestamps); + expect(await env.lastSegmentVideoTimestamps).toEqual([3.6, 4.05]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:1.8, +segment-1-1.ts +#EXTINF:1.8, +segment-1-2.ts +#EXTINF:0.45, +segment-1-3.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, audio not lining up with segment boundaries', async () => { + const env = await setUpSegmentationEnvironment({ audio: true }); + + await env.addAudioPacket!(0); + await env.addAudioPacket!(0.45); + await env.addAudioPacket!(0.9); + await env.addAudioPacket!(1.35); + await env.addAudioPacket!(1.8); + expect(env.segmentCount).toBe(0); + await env.addAudioPacket!(2.25); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentAudioTimestamps).toEqual([0, 0.45, 0.9, 1.35]); + await env.addAudioPacket!(2.7); + await env.addAudioPacket!(3.15); + await env.addAudioPacket!(3.6); + expect(env.segmentCount).toBe(1); + await env.addAudioPacket!(4.05); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentAudioTimestamps).toEqual([1.8, 2.25, 2.7, 3.15]); + + await env.output.finalize(); + expect(env.segmentCount).toBe(3); + console.log(await env.lastSegmentAudioTimestamps); + expect(await env.lastSegmentAudioTimestamps).toEqual([3.6, 4.05]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:1.8, +segment-1-1.ts +#EXTINF:1.8, +segment-1-2.ts +#EXTINF:0.45, +segment-1-3.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, dual-track, single segment', async () => { + const env = await setUpSegmentationEnvironment({ video: true, audio: true }); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.5, 'delta'); + await env.addVideoPacket!(1, 'delta'); + await env.addVideoPacket!(1.5, 'delta'); + await env.addVideoPacket!(2, 'key'); + + await env.addAudioPacket!(0); + await env.addAudioPacket!(0.5); + await env.addAudioPacket!(1); + await env.addAudioPacket!(1.5); + expect(env.segmentCount).toBe(0); + await env.addAudioPacket!(2); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1, 1.5]); + expect(await env.lastSegmentAudioTimestamps).toEqual([0, 0.5, 1, 1.5]); + + await env.output.finalize(); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([2]); + expect(await env.lastSegmentAudioTimestamps).toEqual([2]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:2, +segment-1-1.ts +#EXTINF:0, +segment-1-2.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, dual-track, video dictates the segmentation', async () => { + const env = await setUpSegmentationEnvironment({ video: true, audio: true }); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.5, 'delta'); + await env.addVideoPacket!(1, 'delta'); + await env.addVideoPacket!(1.5, 'key'); + await env.addVideoPacket!(2, 'delta'); + await env.addVideoPacket!(2.5, 'delta'); + await env.addVideoPacket!(3, 'delta'); + await env.addVideoPacket!(3.5, 'delta'); + await env.addVideoPacket!(4, 'delta'); + await env.addVideoPacket!(4.5, 'key'); + + await env.addAudioPacket!(0); + await env.addAudioPacket!(0.5); + await env.addAudioPacket!(1); + expect(env.segmentCount).toBe(0); + await env.addAudioPacket!(1.5); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1]); + expect(await env.lastSegmentAudioTimestamps).toEqual([0, 0.5, 1]); + await env.addAudioPacket!(2); + await env.addAudioPacket!(2.5); + await env.addAudioPacket!(3); + await env.addAudioPacket!(3.5); + await env.addAudioPacket!(4); + expect(env.segmentCount).toBe(1); + await env.addAudioPacket!(4.5); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([1.5, 2, 2.5, 3, 3.5, 4]); + expect(await env.lastSegmentAudioTimestamps).toEqual([1.5, 2, 2.5, 3, 3.5, 4]); + + await env.output.finalize(); + expect(env.segmentCount).toBe(3); + expect(await env.lastSegmentVideoTimestamps).toEqual([4.5]); + expect(await env.lastSegmentAudioTimestamps).toEqual([4.5]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:3 + +#EXTINF:1.5, +segment-1-1.ts +#EXTINF:3, +segment-1-2.ts +#EXTINF:0, +segment-1-3.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, dual-track, video dictates the segmentation, inverted', async () => { + const env = await setUpSegmentationEnvironment({ video: true, audio: true }); + + await env.addAudioPacket!(0); + await env.addAudioPacket!(0.5); + await env.addAudioPacket!(1); + await env.addAudioPacket!(1.5); + await env.addAudioPacket!(2); + await env.addAudioPacket!(2.5); + await env.addAudioPacket!(3); + await env.addAudioPacket!(3.5); + await env.addAudioPacket!(4); + await env.addAudioPacket!(4.5); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.5, 'delta'); + await env.addVideoPacket!(1, 'delta'); + await env.addVideoPacket!(1.5, 'key'); + expect(env.segmentCount).toBe(0); + await env.addVideoPacket!(2, 'delta'); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1]); + expect(await env.lastSegmentAudioTimestamps).toEqual([0, 0.5, 1]); + await env.addVideoPacket!(2.5, 'delta'); + await env.addVideoPacket!(3, 'delta'); + await env.addVideoPacket!(3.5, 'delta'); + await env.addVideoPacket!(4, 'delta'); + expect(env.segmentCount).toBe(1); + await env.addVideoPacket!(4.5, 'key'); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([1.5, 2, 2.5, 3, 3.5, 4]); + expect(await env.lastSegmentAudioTimestamps).toEqual([1.5, 2, 2.5, 3, 3.5, 4]); + + await env.output.finalize(); + expect(env.segmentCount).toBe(3); + expect(await env.lastSegmentVideoTimestamps).toEqual([4.5]); + expect(await env.lastSegmentAudioTimestamps).toEqual([4.5]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:3 + +#EXTINF:1.5, +segment-1-1.ts +#EXTINF:3, +segment-1-2.ts +#EXTINF:0, +segment-1-3.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, dual-track, audio ending after video', async () => { + const env = await setUpSegmentationEnvironment({ video: true, audio: true }); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.5, 'delta'); + await env.addVideoPacket!(1, 'delta'); + await env.addVideoPacket!(1.5, 'delta'); + await env.addVideoPacket!(2, 'key'); + await env.addVideoPacket!(2.5, 'delta'); + + await env.addAudioPacket!(0); + await env.addAudioPacket!(0.5); + await env.addAudioPacket!(1); + await env.addAudioPacket!(1.5); + expect(env.segmentCount).toBe(0); + await env.addAudioPacket!(2); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1, 1.5]); + expect(await env.lastSegmentAudioTimestamps).toEqual([0, 0.5, 1, 1.5]); + await env.addAudioPacket!(2.5); + await env.addAudioPacket!(3); + await env.addAudioPacket!(3.5); + expect(env.segmentCount).toBe(1); + + await env.output.finalize(); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([2, 2.5]); + expect(await env.lastSegmentAudioTimestamps).toEqual([2, 2.5, 3.0, 3.5]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:2, +segment-1-1.ts +#EXTINF:1.5, +segment-1-2.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, dual-track, audio ending after video in separate segment', async () => { + const env = await setUpSegmentationEnvironment({ video: true, audio: true }); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.5, 'delta'); + await env.addVideoPacket!(1, 'delta'); + await env.addVideoPacket!(1.5, 'delta'); + await env.addVideoPacket!(2, 'key'); + await env.addVideoPacket!(2.5, 'delta'); + await env.addVideoPacket!(3, 'delta'); + await env.addVideoPacket!(3.5, 'delta'); + await env.addVideoPacket!(4, 'delta'); + await env.addVideoPacket!(4.5, 'delta'); + env.videoSource!.close(); + + await env.addAudioPacket!(0); + await env.addAudioPacket!(0.5); + await env.addAudioPacket!(1); + await env.addAudioPacket!(1.5); + expect(env.segmentCount).toBe(0); + await env.addAudioPacket!(2); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1, 1.5]); + expect(await env.lastSegmentAudioTimestamps).toEqual([0, 0.5, 1, 1.5]); + await env.addAudioPacket!(2.5); + await env.addAudioPacket!(3); + await env.addAudioPacket!(3.5); + await env.addAudioPacket!(4); + await env.addAudioPacket!(4.5); + expect(env.segmentCount).toBe(1); + await env.addAudioPacket!(5); + expect(env.segmentCount).toBe(2); + expect(await env.lastSegmentVideoTimestamps).toEqual([2, 2.5, 3, 3.5, 4, 4.5]); + expect(await env.lastSegmentAudioTimestamps).toEqual([2, 2.5, 3, 3.5, 4, 4.5]); + await env.addAudioPacket!(5.5); + + await env.output.finalize(); + expect(env.segmentCount).toBe(3); + expect(await env.lastSegmentVideoTimestamps).toEqual([]); + expect(await env.lastSegmentAudioTimestamps).toEqual([5, 5.5]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:3 + +#EXTINF:2, +segment-1-1.ts +#EXTINF:3, +segment-1-2.ts +#EXTINF:0.5, +segment-1-3.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, dual-track, end timestamp with duration', async () => { + const env = await setUpSegmentationEnvironment({ video: true, audio: true }); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.5, 'delta'); + await env.addVideoPacket!(1, 'delta'); + await env.addVideoPacket!(1.5, 'delta', 0.25); + + await env.addAudioPacket!(0); + await env.addAudioPacket!(0.5); + await env.addAudioPacket!(1); + await env.addAudioPacket!(1.5, 0.3); + expect(env.segmentCount).toBe(0); + + await env.output.finalize(); + expect(env.segmentCount).toBe(1); + expect(await env.lastSegmentVideoTimestamps).toEqual([0, 0.5, 1, 1.5]); + expect(await env.lastSegmentAudioTimestamps).toEqual([0, 0.5, 1, 1.5]); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:1.8, +segment-1-1.ts + +#EXT-X-ENDLIST +`, + ); +}); + +test('Segmentation, dual-track, closing writes segment', async () => { + const env = await setUpSegmentationEnvironment({ video: true, audio: true }); + + await env.addVideoPacket!(0, 'key'); + await env.addVideoPacket!(0.5, 'delta'); + await env.addVideoPacket!(1, 'delta'); + await env.addVideoPacket!(1.5, 'delta'); + + await env.addAudioPacket!(0); + await env.addAudioPacket!(0.5); + await env.addAudioPacket!(1); + await env.addAudioPacket!(1.5); + + expect(env.segmentCount).toBe(0); + + env.videoSource!.close(); + await new Promise(resolve => setTimeout(resolve, 4)); + + expect(env.segmentCount).toBe(0); + + env.audioSource!.close(); + await new Promise(resolve => setTimeout(resolve, 4)); + + expect(env.segmentCount).toBe(1); + + await env.output.finalize(); + + expect(env.result).toBe(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-TARGETDURATION:2 + +#EXTINF:1.5, +segment-1-1.ts + +#EXT-X-ENDLIST +`, + ); +}); diff --git a/test/node/join-paths.test.ts b/test/node/join-paths.test.ts index 946d121..1c391ce 100644 --- a/test/node/join-paths.test.ts +++ b/test/node/join-paths.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'vitest'; import { joinPaths } from '../../src/misc.js'; -test('joinPaths handles all path combinations correctly', () => { +test('joinPaths', () => { // Simple relative paths expect(joinPaths('path/to/entry.m3u8', 'other.m3u8')).toBe('path/to/other.m3u8'); expect(joinPaths('entry.m3u8', 'other.m3u8')).toBe('other.m3u8'); @@ -36,6 +36,19 @@ test('joinPaths handles all path combinations correctly', () => { expect(joinPaths('path/to/entry.m3u8', './../other.m3u8')).toBe('path/other.m3u8'); expect(joinPaths('path/to/entry.m3u8', '../foo/../other.m3u8')).toBe('path/other.m3u8'); + // URL base path with query parameters + expect(joinPaths('https://example.com/path/to/entry.m3u8?token=abc', 'other.m3u8')) + .toBe('https://example.com/path/to/other.m3u8'); + expect(joinPaths('https://example.com/path/to/entry.m3u8?redirect=/foo/bar', 'other.m3u8')) + .toBe('https://example.com/path/to/other.m3u8'); + expect(joinPaths('https://example.com/path/to/entry.m3u8?token=abc', '/other.m3u8')) + .toBe('https://example.com/other.m3u8'); + expect(joinPaths('https://example.com/path/to/entry.m3u8?token=abc', '../other.m3u8')) + .toBe('https://example.com/path/other.m3u8'); + + // Filesystem paths with ? in the name are preserved + expect(joinPaths('path/to/sus?directory/file.m3u8', 'other.m3u8')).toBe('path/to/sus?directory/other.m3u8'); + // Second argument is a full URL with protocol expect(joinPaths('path/to/entry.m3u8', 'https://example.com/other.m3u8')) .toBe('https://example.com/other.m3u8'); diff --git a/todo.txt b/todo.txt index d7be6ec..6979828 100644 --- a/todo.txt +++ b/todo.txt @@ -1,9 +1,15 @@ -- Retaining Input instances for segments without clogging up memory indefinitely - idea: discontinuities with extra decoder config on the packet. Also, why not just have the packet metadata on the packet? I think that would make things easier overall. -- ALL_FORMATS but for HLS only - - keep input/demuxer.isSupported()? -- getCodecParamterString() hack feels dirty; idk. well its not really a hack but, surface it over a different field? \ No newline at end of file +- getCodecParamterString() hack feels dirty; idk. well its not really a hack but, surface it over a different field? + +- Add an HLS "live mode". Challenge, where does bitrate come from? + +events: +- onPlaylist +- onSegment +- onMaster + +- hls muxer starting offset +- hls demuxer offset by segment number * targetduration \ No newline at end of file