UNPKG

p5

Version:

[![npm version](https://badge.fury.io/js/p5.svg)](https://www.npmjs.com/package/p5)

1,751 lines (1,674 loc) 51.9 kB
import { Element } from './p5.Element.js'; import './p5.File.js'; import '../io/p5.XML.js'; import '../creating_reading-Cr8L2Jnm.js'; import 'colorjs.io/fn'; import '../color/color_spaces/hsb.js'; import '../constants-BRcElHU3.js'; /** * @module DOM * @submodule DOM */ class MediaElement extends Element { constructor(elt, pInst) { super(elt, pInst); const self = this; this.elt.crossOrigin = 'anonymous'; this._prevTime = 0; this._cueIDCounter = 0; this._cues = []; this.pixels = []; this._pixelsState = this; this._pixelDensity = 1; this._modified = false; // Media has an internal canvas that is used when drawing it to the main // canvas. It will need to be updated each frame as the video itself plays. // We don't want to update it every time we draw, however, in case the user // has used load/updatePixels. To handle this, we record the frame drawn to // the internal canvas so we only update it if the frame has changed. this._frameOnCanvas = -1; Object.defineProperty(self, 'src', { get() { const firstChildSrc = self.elt.children[0].src; const srcVal = self.elt.src === window.location.href ? '' : self.elt.src; const ret = firstChildSrc === window.location.href ? srcVal : firstChildSrc; return ret; }, set(newValue) { for (let i = 0; i < self.elt.children.length; i++) { self.elt.removeChild(self.elt.children[i]); } const source = document.createElement('source'); source.src = newValue; elt.appendChild(source); self.elt.src = newValue; self.modified = true; } }); // private _onended callback, set by the method: onended(callback) self._onended = function () { }; self.elt.onended = function () { self._onended(self); }; } /** * Plays audio or video from a media element. * * @chainable * * @example * <div> * <code> * let beat; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display a message. * text('Click to play', 50, 50); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * describe('The text "Click to play" written in black on a gray background. A beat plays when the user clicks the square.'); * } * * // Play the beat when the user presses the mouse. * function mousePressed() { * beat.play(); * } * </code> * </div> */ play() { if (this.elt.currentTime === this.elt.duration) { this.elt.currentTime = 0; } let promise; if (this.elt.readyState > 1) { promise = this.elt.play(); } else { // in Chrome, playback cannot resume after being stopped and must reload this.elt.load(); promise = this.elt.play(); } if (promise && promise.catch) { promise.catch(e => { // if it's an autoplay failure error if (e.name === 'NotAllowedError') { if (typeof IS_MINIFIED === 'undefined') { p5._friendlyAutoplayError(this.src); } else { console.error(e); } } else { // any other kind of error console.error('Media play method encountered an unexpected error', e); } }); } return this; } /** * Stops a media element and sets its current time to 0. * * Calling `media.play()` will restart playing audio/video from the beginning. * * @chainable * * @example * <div> * <code> * let beat; * let isStopped = true; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * describe('The text "Click to start" written in black on a gray background. The beat starts or stops when the user presses the mouse.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display different instructions based on playback. * if (isStopped === true) { * text('Click to start', 50, 50); * } else { * text('Click to stop', 50, 50); * } * } * * // Adjust playback when the user presses the mouse. * function mousePressed() { * if (isStopped === true) { * // If the beat is stopped, play it. * beat.play(); * isStopped = false; * } else { * // If the beat is playing, stop it. * beat.stop(); * isStopped = true; * } * } * </code> * </div> */ stop() { this.elt.pause(); this.elt.currentTime = 0; return this; } /** * Pauses a media element. * * Calling `media.play()` will resume playing audio/video from the moment it paused. * * @chainable * * @example * <div> * <code> * let beat; * let isPaused = true; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * describe('The text "Click to play" written in black on a gray background. The beat plays or pauses when the user clicks the square.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display different instructions based on playback. * if (isPaused === true) { * text('Click to play', 50, 50); * } else { * text('Click to pause', 50, 50); * } * } * * // Adjust playback when the user presses the mouse. * function mousePressed() { * if (isPaused === true) { * // If the beat is paused, * // play it. * beat.play(); * isPaused = false; * } else { * // If the beat is playing, * // pause it. * beat.pause(); * isPaused = true; * } * } * </code> * </div> */ pause() { this.elt.pause(); return this; } /** * Plays the audio/video repeatedly in a loop. * * @chainable * * @example * <div> * <code> * let beat; * let isLooping = false; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * describe('The text "Click to loop" written in black on a gray background. A beat plays repeatedly in a loop when the user clicks. The beat stops when the user clicks again.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display different instructions based on playback. * if (isLooping === true) { * text('Click to stop', 50, 50); * } else { * text('Click to loop', 50, 50); * } * } * * // Adjust playback when the user presses the mouse. * function mousePressed() { * if (isLooping === true) { * // If the beat is looping, stop it. * beat.stop(); * isLooping = false; * } else { * // If the beat is stopped, loop it. * beat.loop(); * isLooping = true; * } * } * </code> * </div> */ loop() { this.elt.setAttribute('loop', true); this.play(); return this; } /** * Stops the audio/video from playing in a loop. * * The media will stop when it finishes playing. * * @chainable * * @example * <div> * <code> * let beat; * let isPlaying = false; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * describe('The text "Click to play" written in black on a gray background. A beat plays when the user clicks. The beat stops when the user clicks again.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display different instructions based on playback. * if (isPlaying === true) { * text('Click to stop', 50, 50); * } else { * text('Click to play', 50, 50); * } * } * * // Adjust playback when the user presses the mouse. * function mousePressed() { * if (isPlaying === true) { * // If the beat is playing, stop it. * beat.stop(); * isPlaying = false; * } else { * // If the beat is stopped, play it. * beat.play(); * isPlaying = true; * } * } * </code> * </div> */ noLoop() { this.elt.removeAttribute('loop'); return this; } /** * Sets up logic to check that autoplay succeeded. * * @private */ _setupAutoplayFailDetection() { const timeout = setTimeout(() => { if (typeof IS_MINIFIED === 'undefined') { p5._friendlyAutoplayError(this.src); } else { console.error(e); } }, 500); this.elt.addEventListener('play', () => clearTimeout(timeout), { passive: true, once: true }); } /** * Sets the audio/video to play once it's loaded. * * The parameter, `shouldAutoplay`, is optional. Calling * `media.autoplay()` without an argument causes the media to play * automatically. If `true` is passed, as in `media.autoplay(true)`, the * media will automatically play. If `false` is passed, as in * `media.autoPlay(false)`, it won't play automatically. * * @param {Boolean} [shouldAutoplay] whether the element should autoplay. * @chainable * * @example * <div class='notest'> * <code> * let video; * * function setup() { * noCanvas(); * * // Call handleVideo() once the video loads. * video = createVideo('assets/fingers.mov', handleVideo); * * describe('A video of fingers walking on a treadmill.'); * } * * // Set the video's size and play it. * function handleVideo() { * video.size(100, 100); * video.autoplay(); * } * </code> * </div> * * <div class='notest'> * <code> * function setup() { * noCanvas(); * * // Load a video, but don't play it automatically. * let video = createVideo('assets/fingers.mov', handleVideo); * * // Play the video when the user clicks on it. * video.mousePressed(handlePress); * * describe('An image of fingers on a treadmill. They start walking when the user double-clicks on them.'); * } * </code> * </div> * * // Set the video's size and playback mode. * function handleVideo() { * video.size(100, 100); * video.autoplay(false); * } * * // Play the video. * function handleClick() { * video.play(); * } */ autoplay(val) { const oldVal = this.elt.getAttribute('autoplay'); this.elt.setAttribute('autoplay', val); // if we turned on autoplay if (val && !oldVal) { // bind method to this scope const setupAutoplayFailDetection = () => this._setupAutoplayFailDetection(); // if media is ready to play, schedule check now if (this.elt.readyState === 4) { setupAutoplayFailDetection(); } else { // otherwise, schedule check whenever it is ready this.elt.addEventListener('canplay', setupAutoplayFailDetection, { passive: true, once: true }); } } return this; } /** * Sets the audio/video volume. * * Calling `media.volume()` without an argument returns the current volume * as a number in the range 0 (off) to 1 (maximum). * * The parameter, `val`, is optional. It's a number that sets the volume * from 0 (off) to 1 (maximum). For example, calling `media.volume(0.5)` * sets the volume to half of its maximum. * * @return {Number} current volume. * * @example * <div> * <code> * let dragon; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('The text "Volume: V" on a gray square with media controls beneath it. The number "V" oscillates between 0 and 1 as the music plays.'); * } * * function draw() { * background(200); * * // Produce a number between 0 and 1. * let n = 0.5 * sin(frameCount * 0.01) + 0.5; * * // Use n to set the volume. * dragon.volume(n); * * // Get the current volume and display it. * let v = dragon.volume(); * * // Round v to 1 decimal place for display. * v = round(v, 1); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the volume. * text(`Volume: ${v}`, 50, 50); * } * </code> * </div> */ /** * @param {Number} val volume between 0.0 and 1.0. * @chainable */ volume(val) { if (typeof val === 'undefined') { return this.elt.volume; } else { this.elt.volume = val; } } /** * Sets the audio/video playback speed. * * The parameter, `val`, is optional. It's a number that sets the playback * speed. 1 plays the media at normal speed, 0.5 plays it at half speed, 2 * plays it at double speed, and so on. -1 plays the media at normal speed * in reverse. * * Calling `media.speed()` returns the current speed as a number. * * Note: Not all browsers support backward playback. Even if they do, * playback might not be smooth. * * @return {Number} current playback speed. * * @example * <div> * <code> * let dragon; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('The text "Speed: S" on a gray square with media controls beneath it. The number "S" oscillates between 0 and 1 as the music plays.'); * } * * function draw() { * background(200); * * // Produce a number between 0 and 2. * let n = sin(frameCount * 0.01) + 1; * * // Use n to set the playback speed. * dragon.speed(n); * * // Get the current speed and display it. * let s = dragon.speed(); * * // Round s to 1 decimal place for display. * s = round(s, 1); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the speed. * text(`Speed: ${s}`, 50, 50); * } * </code> */ /** * @param {Number} speed speed multiplier for playback. * @chainable */ speed(val) { if (typeof val === 'undefined') { return this.presetPlaybackRate || this.elt.playbackRate; } else { if (this.loadedmetadata) { this.elt.playbackRate = val; } else { this.presetPlaybackRate = val; } } } /** * Sets the media element's playback time. * * The parameter, `time`, is optional. It's a number that specifies the * time, in seconds, to jump to when playback begins. * * Calling `media.time()` without an argument returns the number of seconds * the audio/video has played. * * Note: Time resets to 0 when looping media restarts. * * @return {Number} current time (in seconds). * * @example * <div> * <code> * let dragon; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('The text "S seconds" on a gray square with media controls beneath it. The number "S" increases as the song plays.'); * } * * function draw() { * background(200); * * // Get the current playback time. * let s = dragon.time(); * * // Round s to 1 decimal place for display. * s = round(s, 1); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the playback time. * text(`${s} seconds`, 50, 50); * } * </code> * </div> * * <div> * <code> * let dragon; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * // Jump to 2 seconds to start. * dragon.time(2); * * describe('The text "S seconds" on a gray square with media controls beneath it. The number "S" increases as the song plays.'); * } * * function draw() { * background(200); * * // Get the current playback time. * let s = dragon.time(); * * // Round s to 1 decimal place for display. * s = round(s, 1); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the playback time. * text(`${s} seconds`, 50, 50); * } * </code> * </div> */ /** * @param {Number} time time to jump to (in seconds). * @chainable */ time(val) { if (typeof val === 'undefined') { return this.elt.currentTime; } else { this.elt.currentTime = val; return this; } } /** * Returns the audio/video's duration in seconds. * * @return {Number} duration (in seconds). * * @example * <div> * <code> * let dragon; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('The text "S seconds left" on a gray square with media controls beneath it. The number "S" decreases as the song plays.'); * } * * function draw() { * background(200); * * // Calculate the time remaining. * let s = dragon.duration() - dragon.time(); * * // Round s to 1 decimal place for display. * s = round(s, 1); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display the time remaining. * text(`${s} seconds left`, 50, 50); * } * </code> * </div> */ duration() { return this.elt.duration; } _ensureCanvas() { if (!this.canvas) { this.canvas = document.createElement('canvas'); this.drawingContext = this.canvas.getContext('2d'); this.setModified(true); } // Don't update the canvas again if we have already updated the canvas with // the current frame const needsRedraw = this._frameOnCanvas !== this._pInst.frameCount; if (this.loadedmetadata && needsRedraw) { // wait for metadata for w/h if (this.canvas.width !== this.elt.width) { this.canvas.width = this.elt.width; this.canvas.height = this.elt.height; this.width = this.canvas.width; this.height = this.canvas.height; } this.drawingContext.clearRect( 0, 0, this.canvas.width, this.canvas.height); if (this.flipped === true) { this.drawingContext.save(); this.drawingContext.scale(-1, 1); this.drawingContext.translate(-this.canvas.width, 0); } this.drawingContext.drawImage( this.elt, 0, 0, this.canvas.width, this.canvas.height ); if (this.flipped === true) { this.drawingContext.restore(); } this.setModified(true); this._frameOnCanvas = this._pInst.frameCount; } } loadPixels(...args) { this._ensureCanvas(); return p5.Renderer2D.prototype.loadPixels.apply(this, args); } updatePixels(x, y, w, h) { if (this.loadedmetadata) { // wait for metadata this._ensureCanvas(); p5.Renderer2D.prototype.updatePixels.call(this, x, y, w, h); } this.setModified(true); return this; } get(...args) { this._ensureCanvas(); return p5.Renderer2D.prototype.get.apply(this, args); } _getPixel(...args) { this.loadPixels(); return p5.Renderer2D.prototype._getPixel.apply(this, args); } set(x, y, imgOrCol) { if (this.loadedmetadata) { // wait for metadata this._ensureCanvas(); p5.Renderer2D.prototype.set.call(this, x, y, imgOrCol); this.setModified(true); } } copy(...args) { this._ensureCanvas(); fn.copy.apply(this, args); } mask(...args) { this.loadPixels(); this.setModified(true); p5.Image.prototype.mask.apply(this, args); } /** * helper method for web GL mode to figure out if the element * has been modified and might need to be re-uploaded to texture * memory between frames. * @private * @return {boolean} a boolean indicating whether or not the * image has been updated or modified since last texture upload. */ isModified() { return this._modified; } /** * helper method for web GL mode to indicate that an element has been * changed or unchanged since last upload. gl texture upload will * set this value to false after uploading the texture; or might set * it to true if metadata has become available but there is no actual * texture data available yet.. * @param {Boolean} val sets whether or not the element has been * modified. * @private */ setModified(value) { this._modified = value; } /** * Calls a function when the audio/video reaches the end of its playback. * * The element is passed as an argument to the callback function. * * Note: The function won't be called if the media is looping. * * @param {Function} callback function to call when playback ends. * The `p5.MediaElement` is passed as * the argument. * @chainable * * @example * <div> * <code> * let beat; * let isPlaying = false; * let isDone = false; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * beat = createAudio('assets/beat.mp3'); * * // Call handleEnd() when the beat finishes. * beat.onended(handleEnd); * * describe('The text "Click to play" written in black on a gray square. A beat plays when the user clicks. The text "Done!" appears when the beat finishes playing.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * textSize(16); * * // Display different messages based on playback. * if (isDone === true) { * text('Done!', 50, 50); * } else if (isPlaying === false) { * text('Click to play', 50, 50); * } else { * text('Playing...', 50, 50); * } * } * * // Play the beat when the user presses the mouse. * function mousePressed() { * if (isPlaying === false) { * isPlaying = true; * beat.play(); * } * } * * // Set isDone when playback ends. * function handleEnd() { * isDone = false; * } * </code> * </div> */ onended(callback) { this._onended = callback; return this; } /*** CONNECT TO WEB AUDIO API / p5.sound.js ***/ /** * Sends the element's audio to an output. * * The parameter, `audioNode`, can be an `AudioNode` or an object from the * `p5.sound` library. * * If no element is provided, as in `myElement.connect()`, the element * connects to the main output. All connections are removed by the * `.disconnect()` method. * * Note: This method is meant to be used with the p5.sound.js addon library. * * @param {AudioNode|Object} audioNode AudioNode from the Web Audio API, * or an object from the p5.sound library */ connect(obj) { let audioContext, mainOutput; // if p5.sound exists, same audio context if (typeof fn.getAudioContext === 'function') { audioContext = fn.getAudioContext(); mainOutput = p5.soundOut.input; } else { try { audioContext = obj.context; mainOutput = audioContext.destination; } catch (e) { throw 'connect() is meant to be used with Web Audio API or p5.sound.js'; } } // create a Web Audio MediaElementAudioSourceNode if none already exists if (!this.audioSourceNode) { this.audioSourceNode = audioContext.createMediaElementSource(this.elt); // connect to main output when this method is first called this.audioSourceNode.connect(mainOutput); } // connect to object if provided if (obj) { if (obj.input) { this.audioSourceNode.connect(obj.input); } else { this.audioSourceNode.connect(obj); } } else { // otherwise connect to main output of p5.sound / AudioContext this.audioSourceNode.connect(mainOutput); } } /** * Disconnect all Web Audio routing, including to the main output. * * This is useful if you want to re-route the output through audio effects, * for example. * */ disconnect() { if (this.audioSourceNode) { this.audioSourceNode.disconnect(); } else { throw 'nothing to disconnect'; } } /*** SHOW / HIDE CONTROLS ***/ /** * Show the default * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement" target="_blank">HTMLMediaElement</a> * controls. * * Note: The controls vary between web browsers. * * @example * <div> * <code> * function setup() { * createCanvas(100, 100); * * background('cornflowerblue'); * * // Style the text. * textAlign(CENTER); * textSize(50); * * // Display a dragon. * text('🐉', 50, 50); * * // Create a p5.MediaElement using createAudio(). * let dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('A dragon emoji, 🐉, drawn in the center of a blue square. A song plays in the background. Audio controls are displayed beneath the canvas.'); * } * </code> * </div> */ showControls() { // must set style for the element to show on the page this.elt.style['text-align'] = 'inherit'; this.elt.controls = true; } /** * Hide the default * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement" target="_blank">HTMLMediaElement</a> * controls. * * @example * <div> * <code> * let dragon; * let isHidden = false; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * dragon = createAudio('assets/lucky_dragons.mp3'); * * // Show the default media controls. * dragon.showControls(); * * describe('The text "Double-click to hide controls" written in the middle of a gray square. A song plays in the background. Audio controls are displayed beneath the canvas. The controls appear/disappear when the user double-clicks the square.'); * } * * function draw() { * background(200); * * // Style the text. * textAlign(CENTER); * * // Display a different message when controls are hidden or shown. * if (isHidden === true) { * text('Double-click to show controls', 10, 20, 80, 80); * } else { * text('Double-click to hide controls', 10, 20, 80, 80); * } * } * * // Show/hide controls based on a double-click. * function doubleClicked() { * if (isHidden === true) { * dragon.showControls(); * isHidden = false; * } else { * dragon.hideControls(); * isHidden = true; * } * } * </code> * </div> */ hideControls() { this.elt.controls = false; } /** * Schedules a function to call when the audio/video reaches a specific time * during its playback. * * The first parameter, `time`, is the time, in seconds, when the function * should run. This value is passed to `callback` as its first argument. * * The second parameter, `callback`, is the function to call at the specified * cue time. * * The third parameter, `value`, is optional and can be any type of value. * `value` is passed to `callback`. * * Calling `media.addCue()` returns an ID as a string. This is useful for * removing the cue later. * * @param {Number} time cue time to run the callback function. * @param {Function} callback function to call at the cue time. * @param {Object} [value] object to pass as the argument to * `callback`. * @return {Number} id ID of this cue, * useful for `media.removeCue(id)`. * * @example * <div> * <code> * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * let beat = createAudio('assets/beat.mp3'); * * // Play the beat in a loop. * beat.loop(); * * // Schedule a few events. * beat.addCue(0, changeBackground, 'red'); * beat.addCue(2, changeBackground, 'deeppink'); * beat.addCue(4, changeBackground, 'orchid'); * beat.addCue(6, changeBackground, 'lavender'); * * describe('A red square with a beat playing in the background. Its color changes every 2 seconds while the audio plays.'); * } * * // Change the background color. * function changeBackground(c) { * background(c); * } * </code> * </div> */ addCue(time, callback, val) { const id = this._cueIDCounter++; const cue = new Cue(callback, time, id, val); this._cues.push(cue); if (!this.elt.ontimeupdate) { this.elt.ontimeupdate = this._onTimeUpdate.bind(this); } return id; } /** * Removes a callback based on its ID. * * @param {Number} id ID of the cue, created by `media.addCue()`. * * @example * <div> * <code> * let lavenderID; * let isRemoved = false; * * function setup() { * createCanvas(100, 100); * * // Create a p5.MediaElement using createAudio(). * let beat = createAudio('assets/beat.mp3'); * * // Play the beat in a loop. * beat.loop(); * * // Schedule a few events. * beat.addCue(0, changeBackground, 'red'); * beat.addCue(2, changeBackground, 'deeppink'); * beat.addCue(4, changeBackground, 'orchid'); * * // Record the ID of the "lavender" callback. * lavenderID = beat.addCue(6, changeBackground, 'lavender'); * * describe('The text "Double-click to remove lavender." written on a red square. The color changes every 2 seconds while the audio plays. The lavender option is removed when the user double-clicks the square.'); * } * * function draw() { * background(200); * * // Display different instructions based on the available callbacks. * if (isRemoved === false) { * text('Double-click to remove lavender.', 10, 10, 80, 80); * } else { * text('No more lavender.', 10, 10, 80, 80); * } * } * * // Change the background color. * function changeBackground(c) { * background(c); * } * * // Remove the lavender color-change cue when the user double-clicks. * function doubleClicked() { * if (isRemoved === false) { * beat.removeCue(lavenderID); * isRemoved = true; * } * } * </code> * </div> */ removeCue(id) { for (let i = 0; i < this._cues.length; i++) { if (this._cues[i].id === id) { console.log(id); this._cues.splice(i, 1); } } if (this._cues.length === 0) { this.elt.ontimeupdate = null; } } /** * Removes all functions scheduled with `media.addCue()`. * * @example * <div> * <code> * let isChanging = true; * * function setup() { * createCanvas(100, 100); * * background(200); * * // Create a p5.MediaElement using createAudio(). * let beat = createAudio('assets/beat.mp3'); * * // Play the beat in a loop. * beat.loop(); * * // Schedule a few events. * beat.addCue(0, changeBackground, 'red'); * beat.addCue(2, changeBackground, 'deeppink'); * beat.addCue(4, changeBackground, 'orchid'); * beat.addCue(6, changeBackground, 'lavender'); * * describe('The text "Double-click to stop changing." written on a square. The color changes every 2 seconds while the audio plays. The color stops changing when the user double-clicks the square.'); * } * * function draw() { * background(200); * * // Display different instructions based on the available callbacks. * if (isChanging === true) { * text('Double-click to stop changing.', 10, 10, 80, 80); * } else { * text('No more changes.', 10, 10, 80, 80); * } * } * * // Change the background color. * function changeBackground(c) { * background(c); * } * * // Remove cued functions and stop changing colors when the user * // double-clicks. * function doubleClicked() { * if (isChanging === true) { * beat.clearCues(); * isChanging = false; * } * } * </code> * </div> */ clearCues() { this._cues = []; this.elt.ontimeupdate = null; } // private method that checks for cues to be fired if events // have been scheduled using addCue(callback, time). _onTimeUpdate() { const playbackTime = this.time(); for (let i = 0; i < this._cues.length; i++) { const callbackTime = this._cues[i].time; const val = this._cues[i].val; if (this._prevTime < callbackTime && callbackTime <= playbackTime) { // pass the scheduled callbackTime as parameter to the callback this._cues[i].callback(val); } } this._prevTime = playbackTime; } } // Cue inspired by JavaScript setTimeout, and the // Tone.js Transport Timeline Event, MIT License Yotam Mann 2015 tonejs.org // eslint-disable-next-line no-unused-vars class Cue { constructor(callback, time, id, val) { this.callback = callback; this.time = time; this.id = id; this.val = val; } } function media(p5, fn){ /** * Helpers for create methods. */ function addElement(elt, pInst, media) { const node = pInst._userNode ? pInst._userNode : document.body; node.appendChild(elt); const c = media ? new MediaElement(elt, pInst) : new Element(elt, pInst); pInst._elements.push(c); return c; } /** VIDEO STUFF **/ // Helps perform similar tasks for media element methods. function createMedia(pInst, type, src, callback) { const elt = document.createElement(type); // Create source elements from given sources src = src || ''; if (typeof src === 'string') { src = [src]; } for (const mediaSource of src) { const sourceEl = document.createElement('source'); sourceEl.setAttribute('src', mediaSource); elt.appendChild(sourceEl); } // If callback is provided, attach to element if (typeof callback === 'function') { const callbackHandler = () => { callback(); elt.removeEventListener('canplaythrough', callbackHandler); }; elt.addEventListener('canplaythrough', callbackHandler); } const mediaEl = addElement(elt, pInst, true); mediaEl.loadedmetadata = false; // set width and height onload metadata elt.addEventListener('loadedmetadata', () => { mediaEl.width = elt.videoWidth; mediaEl.height = elt.videoHeight; // set elt width and height if not set if (mediaEl.elt.width === 0) mediaEl.elt.width = elt.videoWidth; if (mediaEl.elt.height === 0) mediaEl.elt.height = elt.videoHeight; if (mediaEl.presetPlaybackRate) { mediaEl.elt.playbackRate = mediaEl.presetPlaybackRate; delete mediaEl.presetPlaybackRate; } mediaEl.loadedmetadata = true; }); return mediaEl; } /** * Creates a `&lt;video&gt;` element for simple audio/video playback. * * `createVideo()` returns a new * <a href="#/p5.MediaElement">p5.MediaElement</a> object. Videos are shown by * default. They can be hidden by calling `video.hide()` and drawn to the * canvas using <a href="#/p5/image">image()</a>. * * The first parameter, `src`, is the path the video. If a single string is * passed, as in `'assets/topsecret.mp4'`, a single video is loaded. An array * of strings can be used to load the same video in different formats. For * example, `['assets/topsecret.mp4', 'assets/topsecret.ogv', 'assets/topsecret.webm']`. * This is useful for ensuring that the video can play across different browsers with * different capabilities. See * <a href='https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats'>MDN</a> * for more information about supported formats. * * The second parameter, `callback`, is optional. It's a function to call once * the video is ready to play. * * @param {String|String[]} src path to a video file, or an array of paths for * supporting different browsers. * @param {Function} [callback] function to call once the video is ready to play. * @return {p5.MediaElement} new <a href="#/p5.MediaElement">p5.MediaElement</a> object. * * @example * <div class='notest'> * <code> * function setup() { * noCanvas(); * * // Load a video and add it to the page. * // Note: this may not work in some browsers. * let video = createVideo('assets/small.mp4'); * * // Show the default video controls. * video.showControls(); * * describe('A video of a toy robot with playback controls beneath it.'); * } * </code> * </div> * * <div class='notest'> * <code> * function setup() { * noCanvas(); * * // Load a video and add it to the page. * // Provide an array options for different file formats. * let video = createVideo( * ['assets/small.mp4', 'assets/small.ogv', 'assets/small.webm'] * ); * * // Show the default video controls. * video.showControls(); * * describe('A video of a toy robot with playback controls beneath it.'); * } * </code> * </div> * * <div class='notest'> * <code> * let video; * * function setup() { * noCanvas(); * * // Load a video and add it to the page. * // Provide an array options for different file formats. * // Call mute() once the video loads. * video = createVideo( * ['assets/small.mp4', 'assets/small.ogv', 'assets/small.webm'], * muteVideo * ); * * // Show the default video controls. * video.showControls(); * * describe('A video of a toy robot with playback controls beneath it.'); * } * * // Mute the video once it loads. * function muteVideo() { * video.volume(0); * } * </code> * </div> */ fn.createVideo = function (src, callback) { // p5._validateParameters('createVideo', arguments); return createMedia(this, 'video', src, callback); }; /** AUDIO STUFF **/ /** * Creates a hidden `&lt;audio&gt;` element for simple audio playback. * * `createAudio()` returns a new * <a href="#/p5.MediaElement">p5.MediaElement</a> object. * * The first parameter, `src`, is the path the video. If a single string is * passed, as in `'assets/video.mp4'`, a single video is loaded. An array * of strings can be used to load the same video in different formats. For * example, `['assets/video.mp4', 'assets/video.ogv', 'assets/video.webm']`. * This is useful for ensuring that the video can play across different * browsers with different capabilities. See * <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats" target="_blank">MDN</a> * for more information about supported formats. * * The second parameter, `callback`, is optional. It's a function to call once * the audio is ready to play. * * @param {String|String[]} [src] path to an audio file, or an array of paths * for supporting different browsers. * @param {Function} [callback] function to call once the audio is ready to play. * @return {p5.MediaElement} new <a href="#/p5.MediaElement">p5.MediaElement</a> object. * * @example * <div class='notest'> * <code> * function setup() { * noCanvas(); * * // Load the audio. * let beat = createAudio('assets/beat.mp3'); * * // Show the default audio controls. * beat.showControls(); * * describe('An audio beat plays when the user double-clicks the square.'); * } * </code> * </div> */ fn.createAudio = function (src, callback) { // p5._validateParameters('createAudio', arguments); return createMedia(this, 'audio', src, callback); }; /** CAMERA STUFF **/ fn.VIDEO = 'video'; fn.AUDIO = 'audio'; // from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia // Older browsers might not implement mediaDevices at all, so we set an empty object first if (navigator.mediaDevices === undefined) { navigator.mediaDevices = {}; } // Some browsers partially implement mediaDevices. We can't just assign an object // with getUserMedia as it would overwrite existing properties. // Here, we will just add the getUserMedia property if it's missing. if (navigator.mediaDevices.getUserMedia === undefined) { navigator.mediaDevices.getUserMedia = function (constraints) { // First get ahold of the legacy getUserMedia, if present const getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; // Some browsers just don't implement it - return a rejected promise with an error // to keep a consistent interface if (!getUserMedia) { return Promise.reject( new Error('getUserMedia is not implemented in this browser') ); } // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise return new Promise(function (resolve, reject) { getUserMedia.call(navigator, constraints, resolve, reject); }); }; } /** * Creates a `&lt;video&gt;` element that "captures" the audio/video stream from * the webcam and microphone. * * `createCapture()` returns a new * <a href="#/p5.MediaElement">p5.MediaElement</a> object. Videos are shown by * default. They can be hidden by calling `capture.hide()` and drawn to the * canvas using <a href="#/p5/image">image()</a>. * * The first parameter, `type`, is optional. It sets the type of capture to * use. By default, `createCapture()` captures both audio and video. If `VIDEO` * is passed, as in `createCapture(VIDEO)`, only video will be captured. * If `AUDIO` is passed, as in `createCapture(AUDIO)`, only audio will be * captured. A constraints object can also be passed to customize the stream. * See the <a href="http://w3c.github.io/mediacapture-main/getusermedia.html#media-track-constraints" target="_blank"> * W3C documentation</a> for possible properties. Different browsers support different * properties. * * The 'flipped' property is an optional property which can be set to `{flipped:true}` * to mirror the video output.If it is true then it means that video will be mirrored * or flipped and if nothing is mentioned then by default it will be `false`. * * The second parameter,`callback`, is optional. It's a function to call once * the capture is ready for use. The callback function should have one * parameter, `stream`, that's a * <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaStream" target="_blank">MediaStream</a> object. * * Note: `createCapture()` only works when running a sketch locally or using HTTPS. Learn more * <a href="http://stackoverflow.com/questions/34197653/getusermedia-in-chrome-47-without-using-https" target="_blank">here</a> * and <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia" target="_blank">here</a>. * * @param {(AUDIO|VIDEO|Object)} [type] type of capture, either AUDIO or VIDEO, * or a constraints object. Both video and audio * audio streams are captured by default. * @param {Object} [flipped] flip the capturing video and mirror the output with `{flipped:true}`. By * default it is false. * @param {Function} [callback] function to call once the stream * has loaded. * @return {p5.MediaElement} new <a href="#/p5.MediaElement">p5.MediaElement</a> object. * * @example * <div class='notest'> * <code> * function setup() { * noCanvas(); * * // Create the video capture. * createCapture(VIDEO); * * describe('A video stream from the webcam.'); * } * </code> * </div> * * <div class='notest'> * <code> * let capture; * * function setup() { * createCanvas(100, 100); * * // Create the video capture and hide the element. * capture = createCapture(VIDEO); * capture.hide(); * * describe('A video stream from the webcam with inverted colors.'); * } * * function draw() { * // Draw the video capture within the canvas. * image(capture, 0, 0, width, width * capture.height / capture.width); * * // Invert the colors in the stream. * filter(INVERT); * } * </code> * </div> * <div class='notest'> * <code> * let capture; * * function setup() { * createCanvas(100, 100); * * // Create the video capture with mirrored output. * capture = createCapture(VIDEO,{ flipped:true }); * capture.size(100,100); * * describe('A video stream from the webcam with flipped or mirrored output.'); * } * * </code> * </div> * * <div class='notest norender'> * <code> * function setup() { * createCanvas(480, 120); * * // Create a constraints object. * let constraints = { * video: { * mandatory: { * minWidth: 1280, * minHeight: 720 * }, * optional: [{ maxFrameRate: 10 }] * }, * audio: false * }; * * // Create the video capture. * createCapture(constraints); * * describe('A video stream from the webcam.'); * } * </code> * </div> */ fn.createCapture = function (...args) { // p5._validateParameters('createCapture', args); // return if getUserMedia is not supported by the browser if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) { throw new DOMException('getUserMedia not supported in this browser'); } let useVideo = true; let useAudio = true; let constraints; let callback; let flipped = false; for (const arg of args) { if (arg === fn.VIDEO) useAudio = false; else if (arg === fn.AUDIO) useVideo = false; else if (typeof arg === 'object') { if (arg.flipped !== undefined) { flipped = arg.flipped; delete arg.flipped; } constraints = Object.assign({}, constraints, arg); } else if (typeof arg === 'function') { callback = arg; } } const videoConstraints = { video: useVideo, audio: useAudio }; constraints = Object.assign({}, videoConstraints, constraints); const domElement = document.createElement('video'); // required to work in iOS 11 & up: domElement.setAttribute('playsinline', ''); navigator.mediaDevices.getUserMedia(constraints).then(function (stream) { try { if ('srcObject' in domElement) { domElement.srcObject = stream; } else { domElement.src = window.URL.createObjectURL(stream); } } catch (err) { domElement.src = stream; } }).catch(e => { if (e.name === 'NotFoundError') p5._friendlyError('No webcam found on this device', 'createCapture'); if (e.name === 'NotAllowedError') p5._friendlyError('Access to the camera was denied', 'createCapture'); console.error(e); }); const videoEl = addElement(domElement, this, true); videoEl.loadedmetadata = false; // set width and height onload metadata domElement.addEventListener('loadedmetadata', function () { domElement.play(); if (domElement.width) { videoEl.width = domElement.width; videoEl.height = domElement.height; if (flipped) { videoEl.elt.style.transform = 'scaleX(-1)'; } } else { videoEl.width = videoEl.elt.width = domElement.videoWidth; videoEl.height = videoEl.elt.height = domElement.videoHeight; } videoEl.loadedmetadata = true; if (callback) callback(domElement.srcObject); }); videoEl.flipped = flipped; return videoEl; }; // ============================================================================= // p5.MediaElement additions // ==================