Commit 48bf333c authored by Shik Chen's avatar Shik Chen Committed by Commit Bot

CCA: Produce non-fragmented MP4 if the output is seekable

Guides FFmpeg to generate regular (non-fragmented) MP4 if the output is
seekable.

Bug: 1140852
Test: Pass camera.CCAUIRecordVideo and camera.CCAUIIntent
Change-Id: Icc1e32c9160e4734360a35738479b47345a7ab95
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2513810Reviewed-by: default avatarInker Kuo <inker@chromium.org>
Commit-Queue: Shik Chen <shik@chromium.org>
Cr-Commit-Position: refs/heads/master@{#823473}
parent 478cfe06
...@@ -30,9 +30,9 @@ let FileStream; // eslint-disable-line no-unused-vars ...@@ -30,9 +30,9 @@ let FileStream; // eslint-disable-line no-unused-vars
let FileOps; // eslint-disable-line no-unused-vars let FileOps; // eslint-disable-line no-unused-vars
/** /**
* An emulated stdin device backed by Int8Array. * An emulated input device backed by Int8Array.
*/ */
class StdinDevice { class InputDevice {
/** /**
* @public * @public
*/ */
...@@ -151,9 +151,9 @@ class StdinDevice { ...@@ -151,9 +151,9 @@ class StdinDevice {
} }
/** /**
* An emulated stdout device. * An emulated output device.
*/ */
class StdoutDevice { class OutputDevice {
/** /**
* @param {!AsyncWriter} output Where should the device write to. * @param {!AsyncWriter} output Where should the device write to.
*/ */
...@@ -185,11 +185,29 @@ class StdoutDevice { ...@@ -185,11 +185,29 @@ class StdoutDevice {
const blob = new Blob([buffer.subarray(offset, offset + length)]); const blob = new Blob([buffer.subarray(offset, offset + length)]);
assert( assert(
position === undefined || position === stream.position, position === undefined || position === stream.position,
'stdout is not seekable'); 'combined seek-and-write operation is not supported');
this.output_.write(blob); this.output_.write(blob);
return length; return length;
} }
/**
* Implements the llseek() operation for the emulated device.
* Only SEEK_SET (0) is supported as |whence|. Reference:
* https://emscripten.org/docs/api_reference/Filesystem-API.html#FS.llseek
* @param {!FileStream} stream
* @param {number} offset The offset in bytes relative to |whence|.
* @param {number} whence The reference position to be used.
* @return {number} The resulting file position.
*/
llseek(stream, offset, whence) {
assert(whence === 0, 'only SEEK_SET is supported');
assert(this.output_.seekable());
if (stream.position !== offset) {
this.output_.seek(offset);
}
return offset;
}
/** /**
* Implements the close() operation for the emulated device. * Implements the close() operation for the emulated device.
*/ */
...@@ -211,9 +229,9 @@ class StdoutDevice { ...@@ -211,9 +229,9 @@ class StdoutDevice {
return { return {
open: () => {}, open: () => {},
close: this.close.bind(this), close: this.close.bind(this),
read: () => assertNotReached('read should not be called on stdout'), read: () => assertNotReached('read should not be called on output'),
write: this.write.bind(this), write: this.write.bind(this),
llseek: () => assertNotReached('llseek should not be called on stdout'), llseek: this.llseek.bind(this),
}; };
} }
} }
...@@ -224,15 +242,15 @@ class StdoutDevice { ...@@ -224,15 +242,15 @@ class StdoutDevice {
class Mp4VideoProcessor { class Mp4VideoProcessor {
/** /**
* @param {!AsyncWriter} output The output writer of mp4. * @param {!AsyncWriter} output The output writer of mp4.
* @param {{seekable: boolean}} opts
*/ */
constructor(output) { constructor(output, {seekable}) {
this.output_ = output; this.output_ = output;
this.stdin_ = new StdinDevice(); this.inputDevice_ = new InputDevice();
this.stdout_ = new StdoutDevice(output); this.outputDevice_ = new OutputDevice(output);
this.jobQueue_ = new AsyncJobQueue(); this.jobQueue_ = new AsyncJobQueue();
const config = { const args = [
arguments: [
// Make the procssing pipeline start earlier by shorten the initial // Make the procssing pipeline start earlier by shorten the initial
// analyze durtaion from the default 5s to 1s. This reduce the // analyze durtaion from the default 5s to 1s. This reduce the
// stop-capture lantency significantly for short videos. // stop-capture lantency significantly for short videos.
...@@ -241,13 +259,25 @@ class Mp4VideoProcessor { ...@@ -241,13 +259,25 @@ class Mp4VideoProcessor {
'-f', 'matroska', '-i', 'pipe:0', '-f', 'matroska', '-i', 'pipe:0',
// transcode audio to aac and copy the video // transcode audio to aac and copy the video
'-c:a', 'aac', '-c:v', 'copy', '-c:a', 'aac', '-c:v', 'copy',
// verbose and don't ask anything // show error log only
'-hide_banner', '-loglevel', 'error', '-nostdin', '-y', '-hide_banner', '-loglevel', 'error',
// streaming friendly output // do not ask anything
'-movflags', 'frag_keyframe', '-frag_duration', '100000', '-nostdin', '-y' // eslint-disable-line comma-dangle
// mp4 output to stdout ];
'-f', 'mp4', 'pipe:1' // eslint-disable-line comma-dangle
], // TODO(crbug.com/1140852): Remove non-seekable code path once the Android
// camera intent helper support seek operation.
if (!seekable) {
// Mark unseekable.
args.push('-seekable', '0');
// Produce a fragmented MP4.
args.push('-movflags', 'frag_keyframe', '-frag_duration', '100000');
}
args.push('/output.mp4');
const config = {
arguments: args,
locateFile: (file) => { locateFile: (file) => {
assert(file === 'ffmpeg.wasm'); assert(file === 'ffmpeg.wasm');
return '/js/lib/ffmpeg.wasm'; return '/js/lib/ffmpeg.wasm';
...@@ -262,11 +292,14 @@ class Mp4VideoProcessor { ...@@ -262,11 +292,14 @@ class Mp4VideoProcessor {
// major numbers 1, 3, 5, 6, 64, and 65. Ref: // major numbers 1, 3, 5, 6, 64, and 65. Ref:
// https://github.com/emscripten-core/emscripten/blob/1ed6dd5cfb88d927ec03ecac8756f0273810d5c9/src/library_fs.js#L1331 // https://github.com/emscripten-core/emscripten/blob/1ed6dd5cfb88d927ec03ecac8756f0273810d5c9/src/library_fs.js#L1331
const input = fs.makedev(80, 0); const input = fs.makedev(80, 0);
fs.registerDevice(input, this.stdin_.getFileOps()); fs.registerDevice(input, this.inputDevice_.getFileOps());
fs.mkdev('/dev/stdin', input); fs.mkdev('/dev/stdin', input);
const output = fs.makedev(80, 1); const output = fs.makedev(80, 1);
fs.registerDevice(output, this.stdout_.getFileOps()); fs.registerDevice(output, this.outputDevice_.getFileOps());
fs.mkdev('/dev/stdout', output); fs.mkdev('/output.mp4', output);
fs.symlink('/dev/tty1', '/dev/stdout');
fs.symlink('/dev/tty1', '/dev/stderr'); fs.symlink('/dev/tty1', '/dev/stderr');
const stdin = fs.open('/dev/stdin', 'r'); const stdin = fs.open('/dev/stdin', 'r');
const stdout = fs.open('/dev/stdout', 'w'); const stdout = fs.open('/dev/stdout', 'w');
...@@ -291,7 +324,7 @@ class Mp4VideoProcessor { ...@@ -291,7 +324,7 @@ class Mp4VideoProcessor {
// This is a function to be called by ffmpeg before running read() in C. // This is a function to be called by ffmpeg before running read() in C.
globalThis.waitReadable = (callback) => { globalThis.waitReadable = (callback) => {
this.stdin_.setReadableCallback(callback); this.inputDevice_.setReadableCallback(callback);
}; };
} }
...@@ -302,7 +335,7 @@ class Mp4VideoProcessor { ...@@ -302,7 +335,7 @@ class Mp4VideoProcessor {
write(blob) { write(blob) {
this.jobQueue_.push(async () => { this.jobQueue_.push(async () => {
const buf = await blob.arrayBuffer(); const buf = await blob.arrayBuffer();
this.stdin_.push(new Int8Array(buf)); this.inputDevice_.push(new Int8Array(buf));
}); });
} }
...@@ -311,14 +344,14 @@ class Mp4VideoProcessor { ...@@ -311,14 +344,14 @@ class Mp4VideoProcessor {
* @return {!Promise} Resolved when all write operations are finished. * @return {!Promise} Resolved when all write operations are finished.
*/ */
async close() { async close() {
// Flush and close stdin. // Flush and close the input device.
this.jobQueue_.push(async () => { this.jobQueue_.push(async () => {
this.stdin_.endPush(); this.inputDevice_.endPush();
}); });
await this.jobQueue_.flush(); await this.jobQueue_.flush();
// Wait until stdout is closed. // Wait until the output device is closed.
await this.stdout_.waitClosed(); await this.outputDevice_.waitClosed();
// Flush and close the output writer. // Flush and close the output writer.
await this.output_.close(); await this.output_.close();
......
...@@ -31,7 +31,8 @@ const VideoProcessor = (async () => { ...@@ -31,7 +31,8 @@ const VideoProcessor = (async () => {
*/ */
async function createVideoProcessor(output) { async function createVideoProcessor(output) {
// Comlink proxies all calls asynchronously, including constructors. // Comlink proxies all calls asynchronously, including constructors.
return new (await VideoProcessor)(Comlink.proxy(output)); return new (await VideoProcessor)(
Comlink.proxy(output), {seekable: output.seekable()});
} }
/** /**
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment