mapbox-gl
Version:
A WebGL interactive maps library
237 lines (210 loc) • 7.15 kB
JavaScript
// @flow
import {getVideo, ResourceType} from '../util/ajax.js';
import ImageSource from './image_source.js';
import Texture from '../render/texture.js';
import {ErrorEvent} from '../util/evented.js';
import ValidationError from '../style-spec/error/validation_error.js';
import type Map from '../ui/map.js';
import type Dispatcher from '../util/dispatcher.js';
import type {Evented} from '../util/evented.js';
import type {VideoSourceSpecification} from '../style-spec/types.js';
/**
* A data source containing video.
* See the [Style Specification](https://www.mapbox.com/mapbox-gl-style-spec/#sources-video) for detailed documentation of options.
*
* @example
* // add to map
* map.addSource('some id', {
* type: 'video',
* url: [
* 'https://www.mapbox.com/blog/assets/baltimore-smoke.mp4',
* 'https://www.mapbox.com/blog/assets/baltimore-smoke.webm'
* ],
* coordinates: [
* [-76.54, 39.18],
* [-76.52, 39.18],
* [-76.52, 39.17],
* [-76.54, 39.17]
* ]
* });
*
* // update
* const mySource = map.getSource('some id');
* mySource.setCoordinates([
* [-76.54335737228394, 39.18579907229748],
* [-76.52803659439087, 39.1838364847587],
* [-76.5295386314392, 39.17683392507606],
* [-76.54520273208618, 39.17876344106642]
* ]);
*
* map.removeSource('some id'); // remove
* @see [Example: Add a video](https://www.mapbox.com/mapbox-gl-js/example/video-on-a-map/)
*/
class VideoSource extends ImageSource {
options: VideoSourceSpecification;
urls: Array<string>;
video: HTMLVideoElement;
roundZoom: boolean;
/**
* @private
*/
constructor(id: string, options: VideoSourceSpecification, dispatcher: Dispatcher, eventedParent: Evented) {
super(id, options, dispatcher, eventedParent);
this.roundZoom = true;
this.type = 'video';
this.options = options;
}
load() {
this._loaded = false;
const options = this.options;
this.urls = [];
for (const url of options.urls) {
this.urls.push(this.map._requestManager.transformRequest(url, ResourceType.Source).url);
}
getVideo(this.urls, (err, video) => {
this._loaded = true;
if (err) {
this.fire(new ErrorEvent(err));
} else if (video) {
this.video = video;
this.video.loop = true;
// Prevent the video from taking over the screen in iOS
this.video.setAttribute('playsinline', '');
// Start repainting when video starts playing. hasTransition() will then return
// true to trigger additional frames as long as the videos continues playing.
this.video.addEventListener('playing', () => {
this.map.triggerRepaint();
});
if (this.map) {
this.video.play();
}
this._finishLoading();
}
});
}
/**
* Pauses the video.
*
* @example
* // Assuming a video source identified by video_source_id was added to the map
* const videoSource = map.getSource('video_source_id');
*
* // Pauses the video
* videoSource.pause();
*/
pause() {
if (this.video) {
this.video.pause();
}
}
/**
* Plays the video.
*
* @example
* // Assuming a video source identified by video_source_id was added to the map
* const videoSource = map.getSource('video_source_id');
*
* // Starts the video
* videoSource.play();
*/
play() {
if (this.video) {
this.video.play();
}
}
/**
* Sets playback to a timestamp, in seconds.
* @private
*/
seek(seconds: number) {
if (this.video) {
const seekableRange = this.video.seekable;
if (seconds < seekableRange.start(0) || seconds > seekableRange.end(0)) {
this.fire(new ErrorEvent(new ValidationError(`sources.${this.id}`, null, `Playback for this video can be set only between the ${seekableRange.start(0)} and ${seekableRange.end(0)}-second mark.`)));
} else this.video.currentTime = seconds;
}
}
/**
* Returns the HTML `video` element.
*
* @returns {HTMLVideoElement} The HTML `video` element.
* @example
* // Assuming a video source identified by video_source_id was added to the map
* const videoSource = map.getSource('video_source_id');
*
* videoSource.getVideo(); // <video crossorigin="Anonymous" loop="">...</video>
*/
getVideo(): HTMLVideoElement {
return this.video;
}
onAdd(map: Map) {
if (this.map) return;
this.map = map;
this.load();
if (this.video) {
this.video.play();
this.setCoordinates(this.coordinates);
}
}
/**
* Sets the video's coordinates and re-renders the map.
*
* @method setCoordinates
* @instance
* @memberof VideoSource
* @returns {VideoSource} Returns itself to allow for method chaining.
* @example
* // Add a video source to the map to map
* map.addSource('video_source_id', {
* type: 'video',
* url: [
* 'https://www.mapbox.com/blog/assets/baltimore-smoke.mp4',
* 'https://www.mapbox.com/blog/assets/baltimore-smoke.webm'
* ],
* coordinates: [
* [-76.54, 39.18],
* [-76.52, 39.18],
* [-76.52, 39.17],
* [-76.54, 39.17]
* ]
* });
*
* // Then update the video source coordinates by new coordinates
* const videoSource = map.getSource('video_source_id');
* videoSource.setCoordinates([
* [-76.5433, 39.1857],
* [-76.5280, 39.1838],
* [-76.5295, 39.1768],
* [-76.5452, 39.1787]
* ]);
*/
// setCoordinates inherited from ImageSource
prepare() {
if (Object.keys(this.tiles).length === 0 || this.video.readyState < 2) {
return; // not enough data for current position
}
const context = this.map.painter.context;
const gl = context.gl;
if (!this.texture) {
this.texture = new Texture(context, this.video, gl.RGBA);
this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
this.width = this.video.videoWidth;
this.height = this.video.videoHeight;
} else if (!this.video.paused) {
this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.video);
}
this._prepareData(context);
}
serialize(): VideoSourceSpecification {
return {
type: 'video',
urls: this.urls,
coordinates: this.coordinates
};
}
hasTransition(): boolean {
return this.video && !this.video.paused;
}
}
export default VideoSource;