Media Viewer: Faster preview generation (#3133)
This commit is contained in:
parent
9dd9937a60
commit
a1b0ec5b10
@ -2,9 +2,8 @@ import type { MP4ArrayBuffer, MP4VideoTrack, MP4Info } from 'mp4box';
|
|||||||
import MP4Box, { DataStream } from 'mp4box';
|
import MP4Box, { DataStream } from 'mp4box';
|
||||||
import { requestPart } from './requestPart';
|
import { requestPart } from './requestPart';
|
||||||
|
|
||||||
const META_PART_SIZE = 64 * 1024;
|
const META_PART_SIZE = 128 * 1024;
|
||||||
const MIN_PART_SIZE = 1024;
|
const MIN_PART_SIZE = 1024;
|
||||||
|
|
||||||
enum Status {
|
enum Status {
|
||||||
loading = 'loading',
|
loading = 'loading',
|
||||||
ready = 'ready',
|
ready = 'ready',
|
||||||
@ -19,9 +18,10 @@ export type MP4DecoderConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type MP4DemuxerConfig = {
|
type MP4DemuxerConfig = {
|
||||||
framesPerVideo: number;
|
|
||||||
stepOffset: number;
|
stepOffset: number;
|
||||||
stepMultiplier: number;
|
stepMultiplier: number;
|
||||||
|
isPolyfill: boolean;
|
||||||
|
maxFrames: number;
|
||||||
onConfig: (config: any) => void;
|
onConfig: (config: any) => void;
|
||||||
onChunk: (chunk: any) => void;
|
onChunk: (chunk: any) => void;
|
||||||
};
|
};
|
||||||
@ -33,12 +33,14 @@ export class MP4Demuxer {
|
|||||||
|
|
||||||
private status = Status.loading;
|
private status = Status.loading;
|
||||||
|
|
||||||
private readonly framesPerVideo: number;
|
|
||||||
|
|
||||||
private readonly stepOffset: number;
|
private readonly stepOffset: number;
|
||||||
|
|
||||||
private readonly stepMultiplier: number;
|
private readonly stepMultiplier: number;
|
||||||
|
|
||||||
|
private readonly maxFrames: number;
|
||||||
|
|
||||||
|
private readonly isPolyfill: boolean;
|
||||||
|
|
||||||
private decodedSamples = new Set<string>();
|
private decodedSamples = new Set<string>();
|
||||||
|
|
||||||
private lastSample = 0;
|
private lastSample = 0;
|
||||||
@ -50,14 +52,16 @@ export class MP4Demuxer {
|
|||||||
constructor(url: string, {
|
constructor(url: string, {
|
||||||
onConfig,
|
onConfig,
|
||||||
onChunk,
|
onChunk,
|
||||||
framesPerVideo,
|
|
||||||
stepOffset,
|
stepOffset,
|
||||||
stepMultiplier,
|
stepMultiplier,
|
||||||
|
isPolyfill,
|
||||||
|
maxFrames,
|
||||||
}: MP4DemuxerConfig) {
|
}: MP4DemuxerConfig) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.framesPerVideo = framesPerVideo;
|
|
||||||
this.stepOffset = stepOffset;
|
this.stepOffset = stepOffset;
|
||||||
this.stepMultiplier = stepMultiplier;
|
this.stepMultiplier = stepMultiplier;
|
||||||
|
this.maxFrames = maxFrames;
|
||||||
|
this.isPolyfill = isPolyfill;
|
||||||
this.onConfig = onConfig;
|
this.onConfig = onConfig;
|
||||||
this.onChunk = onChunk;
|
this.onChunk = onChunk;
|
||||||
|
|
||||||
@ -85,13 +89,14 @@ export class MP4Demuxer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadNextFrames(step: number, size: number, partSize: number) {
|
private async loadNextFrames(step: number, duration: number, partSize: number) {
|
||||||
let tick = step * this.stepOffset;
|
let tick = step * this.stepOffset;
|
||||||
let lastSample = 0;
|
let lastSample = 0;
|
||||||
let rap = this.file.seek(tick, true);
|
let rap = this.file.seek(tick, true);
|
||||||
while (this.status !== Status.closed && rap.offset < size) {
|
while (this.status !== Status.closed) {
|
||||||
try {
|
try {
|
||||||
await this.requestPart(rap.offset, partSize);
|
await this.requestPart(rap.offset, partSize);
|
||||||
|
if (tick > duration) break;
|
||||||
if (this.lastSample > 1 && lastSample < this.lastSample) {
|
if (this.lastSample > 1 && lastSample < this.lastSample) {
|
||||||
tick += step * this.stepMultiplier;
|
tick += step * this.stepMultiplier;
|
||||||
lastSample = this.lastSample;
|
lastSample = this.lastSample;
|
||||||
@ -160,9 +165,10 @@ export class MP4Demuxer {
|
|||||||
const duration = info.duration / info.timescale;
|
const duration = info.duration / info.timescale;
|
||||||
|
|
||||||
// If we set a part size too small, the onSamples callback is not called.
|
// If we set a part size too small, the onSamples callback is not called.
|
||||||
const partSize = roundPartSize(track.bitrate / 24);
|
// If we use polyfill, we need to set a smaller part size to avoid decoding multiple frames.
|
||||||
const minStep = duration < 30 ? 2 : 3;
|
const partSizeDivider = this.isPolyfill ? 24 : 12;
|
||||||
const step = Math.max(Math.floor(duration / this.framesPerVideo), minStep);
|
const partSize = roundPartSize(track.bitrate / partSizeDivider);
|
||||||
|
const step = calculateStep(duration, this.maxFrames);
|
||||||
|
|
||||||
// Start demuxing.
|
// Start demuxing.
|
||||||
this.file.setExtractionOptions(track.id, undefined, { nbSamples: 1 });
|
this.file.setExtractionOptions(track.id, undefined, { nbSamples: 1 });
|
||||||
@ -171,7 +177,7 @@ export class MP4Demuxer {
|
|||||||
this.status = Status.ready;
|
this.status = Status.ready;
|
||||||
|
|
||||||
// // Load frames
|
// // Load frames
|
||||||
void this.loadNextFrames(step, track.size, partSize);
|
void this.loadNextFrames(step, duration, partSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSamples(trackId: number, ref: any, samples: any) {
|
private onSamples(trackId: number, ref: any, samples: any) {
|
||||||
@ -212,3 +218,7 @@ export class MP4Demuxer {
|
|||||||
function roundPartSize(size: number) {
|
function roundPartSize(size: number) {
|
||||||
return size + MIN_PART_SIZE - (size % MIN_PART_SIZE);
|
return size + MIN_PART_SIZE - (size % MIN_PART_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calculateStep(duration: number, max: number): number {
|
||||||
|
return Math.round((duration + max) / max);
|
||||||
|
}
|
||||||
|
|||||||
@ -2,10 +2,12 @@ import { requestMutation } from '../fasterdom/fasterdom';
|
|||||||
|
|
||||||
import { callApi } from '../../api/gramjs';
|
import { callApi } from '../../api/gramjs';
|
||||||
import { ApiMediaFormat } from '../../api/types';
|
import { ApiMediaFormat } from '../../api/types';
|
||||||
import { IS_ANDROID, IS_IOS } from '../../util/windowEnvironment';
|
import { IS_ANDROID, IS_IOS, ARE_WEBCODECS_SUPPORTED } from '../../util/windowEnvironment';
|
||||||
import launchMediaWorkers, { MAX_WORKERS } from '../../util/launchMediaWorkers';
|
import launchMediaWorkers, { MAX_WORKERS } from '../../util/launchMediaWorkers';
|
||||||
|
|
||||||
|
const IS_MOBILE = IS_ANDROID || IS_IOS;
|
||||||
const PREVIEW_SIZE_RATIO = (IS_ANDROID || IS_IOS) ? 0.3 : 0.25;
|
const PREVIEW_SIZE_RATIO = (IS_ANDROID || IS_IOS) ? 0.3 : 0.25;
|
||||||
|
const MAX_FRAMES = ARE_WEBCODECS_SUPPORTED && !IS_MOBILE ? 80 : 40;
|
||||||
const PREVIEW_MAX_SIDE = 200;
|
const PREVIEW_MAX_SIDE = 200;
|
||||||
|
|
||||||
const connections = launchMediaWorkers();
|
const connections = launchMediaWorkers();
|
||||||
@ -26,6 +28,7 @@ export class VideoPreview {
|
|||||||
name: 'video-preview:init',
|
name: 'video-preview:init',
|
||||||
args: [
|
args: [
|
||||||
url,
|
url,
|
||||||
|
MAX_FRAMES,
|
||||||
index,
|
index,
|
||||||
MAX_WORKERS,
|
MAX_WORKERS,
|
||||||
this.onFrame.bind(this),
|
this.onFrame.bind(this),
|
||||||
|
|||||||
@ -3,16 +3,21 @@ import type { CancellableCallback } from '../../util/PostMessageConnector';
|
|||||||
import { MP4Demuxer } from './MP4Demuxer';
|
import { MP4Demuxer } from './MP4Demuxer';
|
||||||
import * as LibAVWebCodecs from './polyfill';
|
import * as LibAVWebCodecs from './polyfill';
|
||||||
|
|
||||||
const MAX_PREVIEWS_PER_VIDEO = 300;
|
|
||||||
|
|
||||||
let decoder: any;
|
let decoder: any;
|
||||||
let demuxer: any;
|
let demuxer: any;
|
||||||
let onDestroy: VoidFunction | undefined;
|
let onDestroy: VoidFunction | undefined;
|
||||||
|
|
||||||
let isLoaded = false;
|
let isLoaded = false;
|
||||||
|
|
||||||
async function init(url: string, workerIndex: number, workersTotal: number, onFrame: CancellableCallback) {
|
async function init(
|
||||||
if (!('VideoDecoder' in globalThis)) {
|
url: string,
|
||||||
|
maxFrames: number,
|
||||||
|
workerIndex: number,
|
||||||
|
workersTotal: number,
|
||||||
|
onFrame: CancellableCallback,
|
||||||
|
) {
|
||||||
|
const hasWebCodecs = 'VideoDecoder' in globalThis;
|
||||||
|
if (!hasWebCodecs) {
|
||||||
await loadLibAV();
|
await loadLibAV();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,9 +43,10 @@ async function init(url: string, workerIndex: number, workersTotal: number, onFr
|
|||||||
});
|
});
|
||||||
|
|
||||||
demuxer = new MP4Demuxer(url, {
|
demuxer = new MP4Demuxer(url, {
|
||||||
framesPerVideo: Math.round(MAX_PREVIEWS_PER_VIDEO / workersTotal),
|
|
||||||
stepOffset: workerIndex,
|
stepOffset: workerIndex,
|
||||||
stepMultiplier: workersTotal,
|
stepMultiplier: workersTotal,
|
||||||
|
isPolyfill: !hasWebCodecs,
|
||||||
|
maxFrames,
|
||||||
onConfig(config) {
|
onConfig(config) {
|
||||||
decoder?.configure(config);
|
decoder?.configure(config);
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user