Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an example of frame-by-frame video export #10172

Merged
merged 3 commits into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions debug/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"ecmaFeatures": {
"jsx": true
},
"sourceType": "script"
"sourceType": "module"
},
"rules": {
"flowtype/require-valid-file-annotation": [0],
Expand All @@ -15,7 +15,8 @@
"strict": "off",
"no-restricted-properties": "off",
"no-unused-vars": "off",
"prefer-template": "off"
"prefer-template": "off",
"import/no-unresolved": "off"
},
"env": {
"es6": true,
Expand Down
111 changes: 111 additions & 0 deletions debug/video-export.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html>
<head>
<title>Mapbox GL JS debug page</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
<style>
#map { width: 960px; height: 540px; }
</style>
</head>

<body>
<div id='map'></div>

<script src='../dist/mapbox-gl-dev.js'></script>
<script src='../debug/access_token_generated.js'></script>

<script type="module">
import loadEncoder from 'https://unpkg.com/[email protected]/build/mp4-encoder.js';
import {simd} from "https://unpkg.com/wasm-feature-detect?module";

const map = window.map = new mapboxgl.Map({
container: 'map',
center: [7.533634776071096, 45.486077107185565],
zoom: 13.5,
pitch: 61,
bearing: -160,
style: 'mapbox://styles/mapbox/satellite-v9'
});

async function animate() {
// do all the animations you need to record here
map.easeTo({
bearing: map.getBearing() - 20,
duration: 3000,
easing: t => t
});
// wait for animation to finish
await untilMapEvent('moveend');
}

map.on('load', async () => {
map.addSource('dem', {type: 'raster-dem', url: 'mapbox://mapbox.mapbox-terrain-dem-v1'});
map.setTerrain({source: 'dem', exaggeration: 1.5});

// wait until the map settles
await untilMapEvent('idle');

// uncomment to fine-tune animation without recording:
// animate(); return;

// don't forget to enable WebAssembly SIMD in chrome://flags for faster encoding
const supportsSIMD = await simd();

// initialize H264 video encoder
const Encoder = await loadEncoder({simd: supportsSIMD});

const gl = map.painter.context.gl;
const width = gl.drawingBufferWidth;
const height = gl.drawingBufferHeight;

const encoder = Encoder.create({
width,
height,
fps: 60,
kbps: 64000,
rgbFlipY: true
});

// stub performance.now for deterministic rendering per-frame (only available in dev build)
let now = performance.now();
mapboxgl.setNow(now);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only available with dev builds, I believe. This example isn't directly usable with the built version of GL JS on CDN or NPM. That's fine for this debug page, but it's good to note.

Copy link
Member Author

@mourner mourner Dec 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanhamley yes, I mentioned this in the PR description and the code comment above.

Copy link
Member Author

@mourner mourner Dec 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added another commit that documents & exposes these methods in production build, so we can add the official example after this gets into a release. Note that it no longer falls back to Date.now because it should be universally supported now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops I missed that in the PR description.

Note that it no longer falls back to Date.now because it should be universally supported now.

👍 awesome


const ptr = encoder.getRGBPointer(); // keep a pointer to encoder WebAssembly heap memory

function frame() {
// increment stub time by 16.6ms (60 fps)
now += 1000 / 60;
mapboxgl.setNow(now);

const pixels = encoder.memory().subarray(ptr); // get a view into encoder memory
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); // read pixels into encoder
encoder.encodeRGBPointer(); // encode the frame
}

map.on('render', frame); // set up frame-by-frame recording

await animate(); // run all the animations

// stop recording
map.off('render', frame);
mapboxgl.restoreNow();

// download the encoded video file
const mp4 = encoder.end();
const anchor = document.createElement("a");
anchor.href = URL.createObjectURL(new Blob([mp4], {type: "video/mp4"}));
anchor.download = "mapbox-gl";
anchor.click();

// make sure to run `ffmpeg -i mapbox-gl.mp4 mapbox-gl-optimized.mp4` to compress the video
});

function untilMapEvent(type) {
return new Promise(resolve => map.once(type, resolve));
}

</script>
</body>
</html>
15 changes: 13 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,22 @@ const exported = {
clearTileCache(callback);
},

workerUrl: ''
workerUrl: '',

/**
* Sets the time used by GL JS internally for all animations. Useful for generating videos from GL JS.
* @var {number} time
*/
setNow: browser.setNow,

/**
* Restores the internal animation timing to follow regular computer time (`performance.now()`).
*/
restoreNow: browser.restoreNow
};

//This gets automatically stripped out in production builds.
Debug.extend(exported, {isSafari, getPerformanceMetrics: PerformanceUtils.getPerformanceMetrics, getPerformanceMetricsAsync: WorkerPerformanceUtils.getPerformanceMetricsAsync, setNow: browser.setNow, restoreNow: browser.restoreNow});
Debug.extend(exported, {isSafari, getPerformanceMetrics: PerformanceUtils.getPerformanceMetrics, getPerformanceMetricsAsync: WorkerPerformanceUtils.getPerformanceMetricsAsync});

/**
* The version of Mapbox GL JS in use as specified in `package.json`,
Expand Down
20 changes: 11 additions & 9 deletions src/util/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
import window from './window';
import type {Cancelable} from '../types/cancelable';

const now = window.performance && window.performance.now ?
window.performance.now.bind(window.performance) :
Date.now.bind(Date);

const raf = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
Expand All @@ -23,26 +19,32 @@ let reducedMotionQuery: MediaQueryList;

let errorState = false;

let stubTime;

/**
* @private
*/
const exported = {
/**
* Provides a function that outputs milliseconds: either performance.now()
* or a fallback to Date.now()
* Returns either performance.now() or a value set by setNow
*/
now,
now(): number {
if (stubTime !== undefined) {
return stubTime;
}
return window.performance.now();
},

setErrorState() {
errorState = true;
},

setNow(time: number) {
exported.now = () => time;
stubTime = time;
},

restoreNow() {
exported.now = now;
stubTime = undefined;
},

frame(fn: (paintStartTimestamp: number) => void): Cancelable {
Expand Down