mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-09 16:53:47 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b06f510382 | ||
|
|
14ad80df5d | ||
|
|
514a533445 | ||
|
|
9072a7a9f7 | ||
|
|
fba463f46b | ||
|
|
3621c59034 | ||
|
|
a16911874c |
@@ -75,6 +75,7 @@ export default withMermaid({
|
|||||||
{ icon: 'github', link: 'https://github.com/Vanilagy/mediabunny' },
|
{ icon: 'github', link: 'https://github.com/Vanilagy/mediabunny' },
|
||||||
{ icon: 'discord', link: 'https://discord.gg/hmpkyYuS4U' },
|
{ icon: 'discord', link: 'https://discord.gg/hmpkyYuS4U' },
|
||||||
{ icon: 'x', link: 'https://x.com/vanilagy' },
|
{ icon: 'x', link: 'https://x.com/vanilagy' },
|
||||||
|
{ icon: 'bluesky', link: 'https://bsky.app/profile/vanilagy.bsky.social' },
|
||||||
],
|
],
|
||||||
|
|
||||||
search: {
|
search: {
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ ESM is preferred because it gives you tree shaking.
|
|||||||
|
|
||||||
You can also just include the library using a script tag in your HTML:
|
You can also just include the library using a script tag in your HTML:
|
||||||
```html
|
```html
|
||||||
<script src="path/to/mediabunny.js"></script>
|
<script src="mediabunny.cjs"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
You can download the built distribution file from the [releases page](https://github.com/Vanilagy/mediabunny/releases).
|
This will add a `Mediabunny` object to the global scope.
|
||||||
|
|
||||||
|
You can download a built distribution file from the [releases page](https://github.com/Vanilagy/mediabunny/releases). Use the `*.cjs` builds for normal script tag inclusion, or the `*.mjs` builds for script tags with `type="module"` or direct imports via ESM. Including the `mediabunny.d.ts` declaration file in your TypeScript project will declare a global `Mediabunny` namespace.
|
||||||
+1
-1
@@ -237,7 +237,7 @@ await conversion.execute();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col-reverse lg:flex-row gap-4 lg:gap-20 lg:items-center">
|
<div class="flex flex-col-reverse lg:flex-row gap-4 lg:gap-20 items-center">
|
||||||
<div class="relative flex-1 min-w-0">
|
<div class="relative flex-1 min-w-0">
|
||||||
<div class="absolute size-70 rounded-full bg-[#ff45ac]/0 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 blur-[200px]" />
|
<div class="absolute size-70 rounded-full bg-[#ff45ac]/0 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 blur-[200px]" />
|
||||||
<img class="relative" src="./assets/inspiring-io.svg">
|
<img class="relative" src="./assets/inspiring-io.svg">
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"version": "1.0.4",
|
"version": "1.0.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"version": "1.0.4",
|
"version": "1.0.6",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/dom-mediacapture-transform": "^0.1.11",
|
"@types/dom-mediacapture-transform": "^0.1.11",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.0.4",
|
"version": "1.0.6",
|
||||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/bundles/mediabunny.cjs",
|
"main": "./dist/bundles/mediabunny.cjs",
|
||||||
|
|||||||
+5
-3
@@ -13,6 +13,7 @@ import { IsobmffReader } from './isobmff/isobmff-reader';
|
|||||||
import { EBMLId, EBMLReader } from './matroska/ebml';
|
import { EBMLId, EBMLReader } from './matroska/ebml';
|
||||||
import { MatroskaDemuxer } from './matroska/matroska-demuxer';
|
import { MatroskaDemuxer } from './matroska/matroska-demuxer';
|
||||||
import { Mp3Demuxer } from './mp3/mp3-demuxer';
|
import { Mp3Demuxer } from './mp3/mp3-demuxer';
|
||||||
|
import { FRAME_HEADER_SIZE } from './mp3/mp3-misc';
|
||||||
import { Mp3Reader } from './mp3/mp3-reader';
|
import { Mp3Reader } from './mp3/mp3-reader';
|
||||||
import { OggDemuxer } from './ogg/ogg-demuxer';
|
import { OggDemuxer } from './ogg/ogg-demuxer';
|
||||||
import { OggReader } from './ogg/ogg-reader';
|
import { OggReader } from './ogg/ogg-reader';
|
||||||
@@ -245,9 +246,10 @@ export class Mp3InputFormat extends InputFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fine, we found one frame header, but we're still not entirely sure this is MP3. Let's check if we can find
|
// Fine, we found one frame header, but we're still not entirely sure this is MP3. Let's check if we can find
|
||||||
// another header nearby:
|
// another header right after it:
|
||||||
mp3Reader.pos = firstHeader.startPos + firstHeader.totalSize;
|
mp3Reader.pos = firstHeader.startPos + firstHeader.totalSize;
|
||||||
const secondHeader = mp3Reader.readNextFrameHeader(Math.min(framesStartPos + 4096, sourceSize));
|
await mp3Reader.reader.loadRange(mp3Reader.pos, mp3Reader.pos + FRAME_HEADER_SIZE);
|
||||||
|
const secondHeader = mp3Reader.readNextFrameHeader(mp3Reader.pos + FRAME_HEADER_SIZE);
|
||||||
if (!secondHeader) {
|
if (!secondHeader) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -257,7 +259,7 @@ export class Mp3InputFormat extends InputFormat {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have found two matching MP3 frames, a strong indicator that this is an MP3 file
|
// We have found two matching consecutive MP3 frames, a strong indicator that this is an MP3 file
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
isIso639Dash2LanguageCode,
|
isIso639Dash2LanguageCode,
|
||||||
roundToMultiple,
|
roundToMultiple,
|
||||||
normalizeRotation,
|
normalizeRotation,
|
||||||
|
Bitstream,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||||
import { Reader } from '../reader';
|
import { Reader } from '../reader';
|
||||||
@@ -74,6 +75,7 @@ type InternalTrack = {
|
|||||||
fragmentLookupTable: FragmentLookupTableEntry[] | null;
|
fragmentLookupTable: FragmentLookupTableEntry[] | null;
|
||||||
currentFragmentState: FragmentTrackState | null;
|
currentFragmentState: FragmentTrackState | null;
|
||||||
fragments: Fragment[];
|
fragments: Fragment[];
|
||||||
|
fragmentsWithKeyFrame: Fragment[];
|
||||||
/** The segment durations of all edit list entries leading up to the main one (from which the offset is taken.) */
|
/** The segment durations of all edit list entries leading up to the main one (from which the offset is taken.) */
|
||||||
editListPreviousSegmentDurations: number;
|
editListPreviousSegmentDurations: number;
|
||||||
/** The media time offset of the main edit list entry (with media time !== -1) */
|
/** The media time offset of the main edit list entry (with media time !== -1) */
|
||||||
@@ -167,6 +169,7 @@ type FragmentTrackState = {
|
|||||||
type FragmentTrackData = {
|
type FragmentTrackData = {
|
||||||
startTimestamp: number;
|
startTimestamp: number;
|
||||||
endTimestamp: number;
|
endTimestamp: number;
|
||||||
|
firstKeyFrameTimestamp: number | null;
|
||||||
samples: FragmentTrackSample[];
|
samples: FragmentTrackSample[];
|
||||||
presentationTimestamps: {
|
presentationTimestamps: {
|
||||||
presentationTimestamp: number;
|
presentationTimestamp: number;
|
||||||
@@ -607,6 +610,7 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
fragmentLookupTable: null,
|
fragmentLookupTable: null,
|
||||||
currentFragmentState: null,
|
currentFragmentState: null,
|
||||||
fragments: [],
|
fragments: [],
|
||||||
|
fragmentsWithKeyFrame: [],
|
||||||
editListPreviousSegmentDurations: 0,
|
editListPreviousSegmentDurations: 0,
|
||||||
editListOffset: 0,
|
editListOffset: 0,
|
||||||
} satisfies InternalTrack as InternalTrack;
|
} satisfies InternalTrack as InternalTrack;
|
||||||
@@ -701,7 +705,10 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (relevantEntryFound) {
|
if (relevantEntryFound) {
|
||||||
throw new Error('Unsupported edit list: multiple edits are not supported.');
|
console.warn(
|
||||||
|
'Unsupported edit list: multiple edits are not currently supported. Only using first edit.',
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaTime === -1) {
|
if (mediaTime === -1) {
|
||||||
@@ -710,7 +717,8 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mediaRate !== 1) {
|
if (mediaRate !== 1) {
|
||||||
throw new Error('Unsupported edit list: media rate must be 1.');
|
console.warn('Unsupported edit list entry: media rate must be 1.');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
track.editListPreviousSegmentDurations = previousSegmentDurations;
|
track.editListPreviousSegmentDurations = previousSegmentDurations;
|
||||||
@@ -942,7 +950,8 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
} else if (sampleSize === 16) {
|
} else if (sampleSize === 16) {
|
||||||
track.info.codec = 'pcm-s16be';
|
track.info.codec = 'pcm-s16be';
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported sample size ${sampleSize} for codec 'twos'.`);
|
console.warn(`Unsupported sample size ${sampleSize} for codec 'twos'.`);
|
||||||
|
track.info.codec = null;
|
||||||
}
|
}
|
||||||
} else if (lowercaseBoxName === 'sowt') {
|
} else if (lowercaseBoxName === 'sowt') {
|
||||||
if (sampleSize === 8) {
|
if (sampleSize === 8) {
|
||||||
@@ -950,7 +959,8 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
} else if (sampleSize === 16) {
|
} else if (sampleSize === 16) {
|
||||||
track.info.codec = 'pcm-s16';
|
track.info.codec = 'pcm-s16';
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported sample size ${sampleSize} for codec 'sowt'.`);
|
console.warn(`Unsupported sample size ${sampleSize} for codec 'sowt'.`);
|
||||||
|
track.info.codec = null;
|
||||||
}
|
}
|
||||||
} else if (lowercaseBoxName === 'raw ') {
|
} else if (lowercaseBoxName === 'raw ') {
|
||||||
track.info.codec = 'pcm-u8';
|
track.info.codec = 'pcm-u8';
|
||||||
@@ -1193,7 +1203,8 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
} else if (pcmSampleSize === 32) {
|
} else if (pcmSampleSize === 32) {
|
||||||
track.info.codec = 'pcm-s32';
|
track.info.codec = 'pcm-s32';
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
console.warn(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
||||||
|
track.info.codec = null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (pcmSampleSize === 16) {
|
if (pcmSampleSize === 16) {
|
||||||
@@ -1203,7 +1214,8 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
} else if (pcmSampleSize === 32) {
|
} else if (pcmSampleSize === 32) {
|
||||||
track.info.codec = 'pcm-s32be';
|
track.info.codec = 'pcm-s32be';
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
console.warn(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
||||||
|
track.info.codec = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (track.info.codec === 'pcm-f32be') {
|
} else if (track.info.codec === 'pcm-f32be') {
|
||||||
@@ -1215,7 +1227,8 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
} else if (pcmSampleSize === 64) {
|
} else if (pcmSampleSize === 64) {
|
||||||
track.info.codec = 'pcm-f64';
|
track.info.codec = 'pcm-f64';
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
console.warn(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
||||||
|
track.info.codec = null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (pcmSampleSize === 32) {
|
if (pcmSampleSize === 32) {
|
||||||
@@ -1223,7 +1236,8 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
} else if (pcmSampleSize === 64) {
|
} else if (pcmSampleSize === 64) {
|
||||||
track.info.codec = 'pcm-f64be';
|
track.info.codec = 'pcm-f64be';
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
console.warn(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
||||||
|
track.info.codec = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1405,8 +1419,27 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
}; break;
|
}; break;
|
||||||
|
|
||||||
case 'stz2': {
|
case 'stz2': {
|
||||||
throw new Error('Unsupported.');
|
const track = this.currentTrack;
|
||||||
};
|
assert(track);
|
||||||
|
|
||||||
|
if (!track.sampleTable) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.metadataReader.pos += 4; // Version + flags
|
||||||
|
this.metadataReader.pos += 3; // Reserved
|
||||||
|
|
||||||
|
const fieldSize = this.metadataReader.readU8(); // in bits
|
||||||
|
const sampleCount = this.metadataReader.readU32();
|
||||||
|
|
||||||
|
const bytes = this.metadataReader.readBytes(Math.ceil(sampleCount * fieldSize / 8));
|
||||||
|
const bitstream = new Bitstream(bytes);
|
||||||
|
|
||||||
|
for (let i = 0; i < sampleCount; i++) {
|
||||||
|
const sampleSize = bitstream.readBits(fieldSize);
|
||||||
|
track.sampleTable.sampleSizes.push(sampleSize);
|
||||||
|
}
|
||||||
|
}; break;
|
||||||
|
|
||||||
case 'stss': {
|
case 'stss': {
|
||||||
const track = this.currentTrack;
|
const track = this.currentTrack;
|
||||||
@@ -1635,6 +1668,16 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
);
|
);
|
||||||
this.currentTrack.fragments.splice(insertionIndex + 1, 0, this.currentFragment);
|
this.currentTrack.fragments.splice(insertionIndex + 1, 0, this.currentFragment);
|
||||||
|
|
||||||
|
const hasKeyFrame = trackData.firstKeyFrameTimestamp !== null;
|
||||||
|
if (hasKeyFrame) {
|
||||||
|
const insertionIndex = binarySearchLessOrEqual(
|
||||||
|
this.currentTrack.fragmentsWithKeyFrame,
|
||||||
|
this.currentFragment.moofOffset,
|
||||||
|
x => x.moofOffset,
|
||||||
|
);
|
||||||
|
this.currentTrack.fragmentsWithKeyFrame.splice(insertionIndex + 1, 0, this.currentFragment);
|
||||||
|
}
|
||||||
|
|
||||||
const { currentFragmentState } = this.currentTrack;
|
const { currentFragmentState } = this.currentTrack;
|
||||||
assert(currentFragmentState);
|
assert(currentFragmentState);
|
||||||
|
|
||||||
@@ -1733,7 +1776,8 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
assert(track.currentFragmentState);
|
assert(track.currentFragmentState);
|
||||||
|
|
||||||
if (this.currentFragment.trackData.has(track.id)) {
|
if (this.currentFragment.trackData.has(track.id)) {
|
||||||
throw new Error('Can\'t have two trun boxes for the same track in one fragment.');
|
console.warn('Can\'t have two trun boxes for the same track in one fragment. Ignoring...');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = this.metadataReader.readU8();
|
const version = this.metadataReader.readU8();
|
||||||
@@ -1770,6 +1814,7 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
const trackData: FragmentTrackData = {
|
const trackData: FragmentTrackData = {
|
||||||
startTimestamp: 0,
|
startTimestamp: 0,
|
||||||
endTimestamp: 0,
|
endTimestamp: 0,
|
||||||
|
firstKeyFrameTimestamp: null,
|
||||||
samples: [],
|
samples: [],
|
||||||
presentationTimestamps: [],
|
presentationTimestamps: [],
|
||||||
startTimestampIsFinal: false,
|
startTimestampIsFinal: false,
|
||||||
@@ -1831,13 +1876,19 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
.map((x, i) => ({ presentationTimestamp: x.presentationTimestamp, sampleIndex: i }))
|
.map((x, i) => ({ presentationTimestamp: x.presentationTimestamp, sampleIndex: i }))
|
||||||
.sort((a, b) => a.presentationTimestamp - b.presentationTimestamp);
|
.sort((a, b) => a.presentationTimestamp - b.presentationTimestamp);
|
||||||
|
|
||||||
// Update sample durations based on presentation order
|
for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
|
||||||
for (let i = 0; i < trackData.presentationTimestamps.length - 1; i++) {
|
const currentEntry = trackData.presentationTimestamps[i]!;
|
||||||
const current = trackData.presentationTimestamps[i]!;
|
const currentSample = trackData.samples[currentEntry.sampleIndex]!;
|
||||||
const next = trackData.presentationTimestamps[i + 1]!;
|
|
||||||
|
|
||||||
const duration = next.presentationTimestamp - current.presentationTimestamp;
|
if (trackData.firstKeyFrameTimestamp === null && currentSample.isKeyFrame) {
|
||||||
trackData.samples[current.sampleIndex]!.duration = duration;
|
trackData.firstKeyFrameTimestamp = currentSample.presentationTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < trackData.presentationTimestamps.length - 1) {
|
||||||
|
// Update sample durations based on presentation order
|
||||||
|
const nextEntry = trackData.presentationTimestamps[i + 1]!;
|
||||||
|
currentSample.duration = nextEntry.presentationTimestamp - currentEntry.presentationTimestamp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstSample = trackData.samples[trackData.presentationTimestamps[0]!.sampleIndex]!;
|
const firstSample = trackData.samples[trackData.presentationTimestamps[0]!.sampleIndex]!;
|
||||||
@@ -2074,13 +2125,13 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
correctSampleFound: true,
|
correctSampleFound: true,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Walk the list of fragments until we find the next fragment for this track
|
// Walk the list of fragments until we find the next fragment for this track with a key frame
|
||||||
let currentFragment = locationInFragment.fragment;
|
let currentFragment = locationInFragment.fragment;
|
||||||
while (currentFragment.nextFragment) {
|
while (currentFragment.nextFragment) {
|
||||||
currentFragment = currentFragment.nextFragment;
|
currentFragment = currentFragment.nextFragment;
|
||||||
|
|
||||||
const trackData = currentFragment.trackData.get(this.internalTrack.id);
|
const trackData = currentFragment.trackData.get(this.internalTrack.id);
|
||||||
if (trackData) {
|
if (trackData && trackData.firstKeyFrameTimestamp !== null) {
|
||||||
const fragmentIndex = binarySearchExact(
|
const fragmentIndex = binarySearchExact(
|
||||||
this.internalTrack.fragments,
|
this.internalTrack.fragments,
|
||||||
currentFragment.moofOffset,
|
currentFragment.moofOffset,
|
||||||
@@ -2089,9 +2140,7 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
assert(fragmentIndex !== -1);
|
assert(fragmentIndex !== -1);
|
||||||
|
|
||||||
const keyFrameIndex = trackData.samples.findIndex(x => x.isKeyFrame);
|
const keyFrameIndex = trackData.samples.findIndex(x => x.isKeyFrame);
|
||||||
if (keyFrameIndex === -1) {
|
assert(keyFrameIndex !== -1); // There must be one
|
||||||
throw new Error('Not supported: Fragment does not contain key sample.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fragmentIndex,
|
fragmentIndex,
|
||||||
@@ -2231,26 +2280,34 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private findKeySampleInFragmentsForTimestamp(timestampInTimescale: number) {
|
private findKeySampleInFragmentsForTimestamp(timestampInTimescale: number) {
|
||||||
const fragmentIndex = binarySearchLessOrEqual(
|
const indexInKeyFrameFragments = binarySearchLessOrEqual(
|
||||||
// This array is technically not sorted by start timestamp, but for any reasonable file, it basically is.
|
// This array is technically not sorted by start timestamp, but for any reasonable file, it basically is.
|
||||||
this.internalTrack.fragments,
|
this.internalTrack.fragmentsWithKeyFrame,
|
||||||
timestampInTimescale,
|
timestampInTimescale,
|
||||||
x => x.trackData.get(this.internalTrack.id)!.startTimestamp,
|
x => x.trackData.get(this.internalTrack.id)!.startTimestamp,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let fragmentIndex = -1;
|
||||||
let sampleIndex = -1;
|
let sampleIndex = -1;
|
||||||
let correctSampleFound = false;
|
let correctSampleFound = false;
|
||||||
|
|
||||||
if (fragmentIndex !== -1) {
|
if (indexInKeyFrameFragments !== -1) {
|
||||||
const fragment = this.internalTrack.fragments[fragmentIndex]!;
|
const fragment = this.internalTrack.fragmentsWithKeyFrame[indexInKeyFrameFragments]!;
|
||||||
|
|
||||||
|
// Now, let's find the actual index of the fragment in the list of ALL fragments, not just key frame ones
|
||||||
|
fragmentIndex = binarySearchExact(
|
||||||
|
this.internalTrack.fragments,
|
||||||
|
fragment.moofOffset,
|
||||||
|
x => x.moofOffset,
|
||||||
|
);
|
||||||
|
assert(fragmentIndex !== -1);
|
||||||
|
|
||||||
const trackData = fragment.trackData.get(this.internalTrack.id)!;
|
const trackData = fragment.trackData.get(this.internalTrack.id)!;
|
||||||
const index = findLastIndex(trackData.presentationTimestamps, (x) => {
|
const index = findLastIndex(trackData.presentationTimestamps, (x) => {
|
||||||
const sample = trackData.samples[x.sampleIndex]!;
|
const sample = trackData.samples[x.sampleIndex]!;
|
||||||
return sample.isKeyFrame && x.presentationTimestamp <= timestampInTimescale;
|
return sample.isKeyFrame && x.presentationTimestamp <= timestampInTimescale;
|
||||||
});
|
});
|
||||||
|
assert(index !== -1); // It's a key frame fragment, so there must be a key frame
|
||||||
if (index === -1) {
|
|
||||||
throw new Error('Not supported: Fragment does not begin with a key sample.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = trackData.presentationTimestamps[index]!;
|
const entry = trackData.presentationTimestamps[index]!;
|
||||||
sampleIndex = entry.sampleIndex;
|
sampleIndex = entry.sampleIndex;
|
||||||
|
|||||||
@@ -484,24 +484,18 @@ export class MatroskaDemuxer extends Demuxer {
|
|||||||
.map((block, i) => ({ timestamp: block.timestamp, blockIndex: i }))
|
.map((block, i) => ({ timestamp: block.timestamp, blockIndex: i }))
|
||||||
.sort((a, b) => a.timestamp - b.timestamp);
|
.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
|
||||||
let hasKeyFrame = false;
|
|
||||||
for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
|
for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
|
||||||
const entry = trackData.presentationTimestamps[i]!;
|
const currentEntry = trackData.presentationTimestamps[i]!;
|
||||||
const block = trackData.blocks[entry.blockIndex]!;
|
const currentBlock = trackData.blocks[currentEntry.blockIndex]!;
|
||||||
|
|
||||||
if (block.isKeyFrame) {
|
if (trackData.firstKeyFrameTimestamp === null && currentBlock.isKeyFrame) {
|
||||||
hasKeyFrame = true;
|
trackData.firstKeyFrameTimestamp = currentBlock.timestamp;
|
||||||
|
|
||||||
if (trackData.firstKeyFrameTimestamp === null && block.isKeyFrame) {
|
|
||||||
trackData.firstKeyFrameTimestamp = block.timestamp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i < trackData.presentationTimestamps.length - 1) {
|
if (i < trackData.presentationTimestamps.length - 1) {
|
||||||
// Update block durations based on presentation order
|
// Update block durations based on presentation order
|
||||||
const nextEntry = trackData.presentationTimestamps[i + 1]!;
|
const nextEntry = trackData.presentationTimestamps[i + 1]!;
|
||||||
const nextBlock = trackData.blocks[nextEntry.blockIndex]!;
|
currentBlock.duration = nextEntry.timestamp - currentBlock.timestamp;
|
||||||
block.duration = nextBlock.timestamp - block.timestamp;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,6 +514,7 @@ export class MatroskaDemuxer extends Demuxer {
|
|||||||
);
|
);
|
||||||
track.clusters.splice(insertionIndex + 1, 0, cluster);
|
track.clusters.splice(insertionIndex + 1, 0, cluster);
|
||||||
|
|
||||||
|
const hasKeyFrame = trackData.firstKeyFrameTimestamp !== null;
|
||||||
if (hasKeyFrame) {
|
if (hasKeyFrame) {
|
||||||
const insertionIndex = binarySearchLessOrEqual(
|
const insertionIndex = binarySearchLessOrEqual(
|
||||||
track.clustersWithKeyFrame,
|
track.clustersWithKeyFrame,
|
||||||
@@ -1210,7 +1205,7 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
|
|||||||
correctBlockFound: true,
|
correctBlockFound: true,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Walk the list of clusters until we find the next cluster for this track
|
// Walk the list of clusters until we find the next cluster for this track with a key frame
|
||||||
let currentCluster = locationInCluster.cluster;
|
let currentCluster = locationInCluster.cluster;
|
||||||
while (currentCluster.nextCluster) {
|
while (currentCluster.nextCluster) {
|
||||||
currentCluster = currentCluster.nextCluster;
|
currentCluster = currentCluster.nextCluster;
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ export const readFrameHeader = (word: number, reader: { pos: number; fileSize: n
|
|||||||
|
|
||||||
reader.pos += 1;
|
reader.pos += 1;
|
||||||
|
|
||||||
|
if (firstByte !== 0xff) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if ((secondByte & 0xe0) !== 0xe0) {
|
if ((secondByte & 0xe0) !== 0xe0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export class Mp3Reader {
|
|||||||
assert(this.fileSize);
|
assert(this.fileSize);
|
||||||
until ??= this.fileSize;
|
until ??= this.fileSize;
|
||||||
|
|
||||||
while (this.pos < until - FRAME_HEADER_SIZE) {
|
while (this.pos <= until - FRAME_HEADER_SIZE) {
|
||||||
const word = this.readU32();
|
const word = this.readU32();
|
||||||
this.pos -= 4;
|
this.pos -= 4;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user