Can HTML5 <video> tag be played in reverse, or do I have to download 2 videos (forward and backward play)?
I'm looking for a solution that avoids a user from downloading 2 videos.
Without even going into HTML5 or Javascript, many video formats are streaming formats that are designed to be played forward. Playing it backwards would require decoding the whole stream, storing each raw frame on the disk to avoid clobbering memory, then rendering the frames backwards.
At least one person actually tried that using mplayer, though, so it can be done, at least in principle.
I managed to do it in an update method. Every frame I decrease video.currentTime to the time elapsed and so far it is the best result I managed to get.
aMediaElement.playbackRate = -1;
UAs may not support this, though it is valid to set playbackRate to a negative value.
This snippet just shows, how it could be done, but it takes some time to copy each frame. Which highly depends on the hardware.
It generates a canvas of each frame it has to play. When it's on 100% the playback starts directly and plays backward, forward... and so on. The original Video is also attached after the generation, but won't play automatically due to iframe rules.
It is fine for short videos and as a proof of concept.
Update:
I changed the order of the capture part so that the frame at max duration is skipped but the one at 0 is captured (forgot it before).
The frame at max duration caused a white canvas on every video i tried.
I also changed the playback to play it in reverse order as soon as the last frame is reached for an endless playback. So you easily see, that this playback is a bit CPU intensive compared to hardware accelerated videos.
fetch('https://i.imgur.com/BPQF5yy.mp4')
.then(res => res.blob())
.then(blob => {
return new Promise((res) => {
const fr = new FileReader();
fr.onload = e => res(fr.result);
fr.readAsDataURL(blob);
})
}).then(async(base64str) => {
const video = document.createElement("video");
video.src = base64str;
video.controls = true;
video.className = "w-50";
while (isNaN(video.duration))
await new Promise((r) => setTimeout(r, 50));
const FPS = 25;
const status = document.createElement("div"),
length = document.createElement("div");
length.innerText = `Generating ${Math.ceil(video.duration / (1 / FPS))} frames for a ${FPS} FPS playback (Video Duration = ${video.duration})`;
document.body.appendChild(length);
document.body.appendChild(status);
const frames = [],
copy = () => {
const c = document.createElement("canvas");
Object.assign(c, {
width: video.videoWidth,
height: video.videoHeight,
className: "w-50"
});
c.getContext("2d").drawImage(video, 0, 0);
return c;
};
// first seek outside of the loop this image won't be copied
video.currentTime = video.duration;
// this frame seems to be always white/transparent
while (video.currentTime) {
if (video.currentTime - 1 / FPS < 0)
video.currentTime = 0;
else
video.currentTime = video.currentTime - 1 / FPS;
await new Promise((next) => {
video.addEventListener('seeked', () => {
frames.push(copy());
status.innerText = (frames.length / (Math.ceil(video.duration / (1 / FPS))) * 100).toFixed(2) + '%';
next();
}, {
once: true
});
});
}
/*
* frames now contains all canvas elements created,
* I just append the first image and replace it on
* every tick with the next frame.
* using last.replaceWith(frames[currentPos]) guarantees a smooth playback.
*/
let i = 0, last = frames[0];
document.body.insertAdjacentHTML('beforeend', `<div class="w-50">Captured</div><div class="w-50">Video</div>`);
document.body.appendChild(frames[0]);
const interval = setInterval(() => {
if (frames[++i]) {
last.replaceWith(frames[i]);
last = frames[i];
} else {
frames.reverse();
i=0;
}
}, 1000 / FPS);
document.body.appendChild(video);
// won't :(
video.play();
});
/* Just for this example */
.w-50 {
width: 50%;
display: inline-block;
}
* {
margin: 0;
padding: 0;
font-family: Sans-Serif;
font-size: 12px;
}
I tried request animation frame, calculated the diff and updated currentTime. This does not work, the video tag doesn't repaint fast enough.
Get the HTMLMediaElement's duration then set an Interval that would run every second and playing the media by setting the .currentTime and decrease the value every second by 1. The media element must be fully downloaded for this to work. I've only tested this on 30-second clips and unsure if faster (lesser than 1sec.) intervals are possible. Note that this method still plays the media forward. Try increasing media playback rate to make it feel more seamless.
Related
When I play a tone and then stop it, unpleasant "crack" is heard and the beginning and the end. I searched for a solution and found out that reducing the volume over time should solve that issue.
Therefore, I use AudioGainMode to ramp up and down, instead of cutting the audio abruptly:
controlGain.gain.exponentialRampToValueAtTime(
1,
gAudioCtx.currentTime+time_milliseconds/1000
);
// and later...
controlGain.gain.exponentialRampToValueAtTime(
0.0001,
gAudioCtx.currentTime+time_milliseconds/1000
);
Because exponential functions are not defined at 0, 0.0001 is used instead.
However, in Firefox, I can still hear nasty cracking. I also noticed that using longer delays has no effect - the gain reaches target value instantly.
function sleep(ms) {
return new Promise(function (resolve, reject) {
setTimeout(resolve, ms);
});
}
function play() {
const AUDIO_RAMP_DELAY = 50;
var gAudioCtx = new AudioContext();
const controlGain = gAudioCtx.createGain();
controlGain.gain.value = 0.00001;
/// Full volume at t+50ms
controlGain.gain.exponentialRampToValueAtTime(
1,
gAudioCtx.currentTime+AUDIO_RAMP_DELAY/1000
);
controlGain.connect(gAudioCtx.destination);
var osc = gAudioCtx.createOscillator();
// create a tone around 440hz
const length = 1024;
var real = new Float32Array(length);
var imag = new Float32Array(length);
real[440]=1;
//real[512]=1;
var wave = gAudioCtx.createPeriodicWave(real, imag, {disableNormalization: true});
osc.frequency.value = 1;
osc.setPeriodicWave(wave);
osc.connect(controlGain);
osc.start();
(async function() {
await sleep(AUDIO_RAMP_DELAY+1);
// now we're at full volume, wait another 2 seconds
await sleep(2000);
controlGain.gain.exponentialRampToValueAtTime(
0.00001,
gAudioCtx.currentTime+50/1000
);
await sleep(2000);
osc.stop();
document.querySelector("button").disabled = false;
})();
}
<h2>Warning: this demo makes loud sounds!</h2>
<button onclick="play(); this.disabled=true">Click to play</button>
How to make it work in firefox as well?
I noticed too that the exponentialRampToValueAtTime() does not seem to work as intended in Firefox. Too me it seems like an incorrect implementation.
A workaround could be to use linearRampToValueAtTime instead.
Alternatively you could also use setValueCurveAtTime to define your own curve.
Scenario:
Audio starts playing from 0:00. At exactly 0:05, the track skips forwards to 0:30.
The track immediately starts playing at 0:30, and at exactly 0:35, the track skips backwards to 0:05 and plays the rest of the audio file
Summary: Play: 0:00 to 0.05, Skip: 0:05 to 0:30, Play: 0:30 to 0:35, Skip: 0:35 to 0:05, Play: 0.05 to END
The real problem comes when there is the need for a immediate and seamless skip. For example, setTimeout is very inaccurate and drifts meaning it cannot be used in this case.
I have tried to use the Web Audio API to accomplish this, but I'm still unable to get an immediate transition. I'm currently scheduling segments of a loaded song using AudioBufferSourceNode.start(when, offset, duration);, initially passing in (0 [when], 0 [offset], 0:05 [duration]) for the example above. After this, I'm still using setTimeout to call the same function to run just short of 0:30 and scheduling something like (0 [when] + 0:05 [previous duration], 0:30 [offset], 0:05 [duration]).
Example of this
var AudioContext = window.AudioContext || window.webkitAudioContext,
audioCtx = new AudioContext();
var skipQueue = [0, 5, 30, 35, 5];
// getSong(); // load in the preview song (https://audiojungle.net/item/inspiring/9325839?s_rank=1)
playSound(0, 0);
function getSong() {
console.log("Loading song...");
request = new XMLHttpRequest();
// request.open('GET', "https://preview.s3.envato.com/files/230851789/preview.mp3?response-content-disposition=attachment%3Bfilename%3D20382637_uplifting-cinematic-epic_by_jwaldenmusic_preview.mp3&Expires=1501966849&Signature=HUMfPw3b4ap13cyrc0ZrNNumb0s4AXr7eKHezyIR-rU845u65oQpxjmZDl8AUZU7cR1KuQGV4TLkQ~egPt5hCiw7SUBRApXw3nnrRdtf~M6PXbNqVYhrhfNq4Y~MgvZdd1NEetv2rCjhirLw4OIhkiC2xH2jvbN6mggnhNnw8ZemBzDH3stCVDTEPGuRgUJrwLwsgBHmy5D2Ef0It~oN8LGG~O~LFB5MGHHmRSjejhjnrfSngWNF9SPI3qn7hOE6WDvcEbNe2vBm5TvEx2OTSlYQc1472rrkGDcxzOHGu9jLEizL-sSiV61uVAp5wqKxd2xNBcsUn3EXXnjMIAmUIQ__&Key-Pair-Id=APKAIEEC7ZU2JC6FKENA", true); // free preview from envato
request.responseType = "arraybuffer";
request.onload = function() {
var audioData = request.response;
audioCtx.decodeAudioData(audioData, function(buffer) {
audioBuffer = buffer;
console.log("Ready to play!");
playSong()
}, function(e) {
"Error with decoding audio data" + e.err
});
}
request.send();
}
function playSound(previousPlay, nextPlay) {
// source = audioCtx.createBufferSource();
// source.buffer = audioBuffer;
// source.connect(audioCtx.destination);
skipQueue.shift();
var duration = Math.abs(skipQueue[0] - previousPlay);
// source.start(nextPlay, previousPlay, duration);
console.log("Running: source.start(" + nextPlay + ", " + previousPlay + ", " + duration + ")");
console.log("Current Time: " + previousPlay);
console.log("Next Play in: " + duration + " (Skipping from " + skipQueue[0] + " to " + skipQueue[1] + ")");
if (skipQueue.length > 1) {
setTimeout(function() {
playSound(skipQueue[0], nextPlay + duration);
}, 1000 * duration - 50); // take 50ms off for drift that'll be corrected in scheduling
}
}
<strong>Expected:</strong><br />
Play: 0:00 to 0.05, Skip: 0:05 to 0:30, Play: 0:30 to 0:35, Skip: 0:35 to 0:05, Play: 0.05 to END
I can't get this simplified down example 100% working, but you can see what my attempt is in the console. I've also commented out code since StackOverflow doesn't support AJAX.
I'm open to using any other API or method that you have in mind!
You don't ever show when you actually call start() on an AudioBufferSourceNode, so I can't explicitly debug. But in short, you need to schedule the stops and starts AHEAD of when they need to happen, not try to "swap" the active sound playing in a setTimeout callback (or try to get them to align once the sound has stopped playing).
For example, you'd do something like:
var bs1 = audioCtx.createBufferSourceNode();
var bs2 = audioCtx.createBufferSourceNode();
var bs3 = audioCtx.createBufferSourceNode();
var now = audioCtx.currentTime + 0.020; // add 20ms for scheduling slop
bs1.buffer = audioBuffer;
bs1.connect( audioCtx.destination );
bs2.buffer = audioBuffer;
bs2.connect( audioCtx.destination );
bs3.buffer = audioBuffer;
bs3.connect( audioCtx.destination );
bs1.start( now, 0, 5 ); // time, offset, duration
bs2.start( now+5, 30, 5 );
bs3.start( now+10, 5 ); // no duration; play to end
If you want to cancel this playing, you'll have to disconnect the buffersource nodes.
The main issue in this will be the accuracy in time and shifting seamlessly from 0:05 to 0:30 and back from 0:35 to 0:05.
I think you should create two different audio sources. Start one from 0:00 and seek the other one at 0:30 but pause it as soon as it starts playing using events.
Then track time using the ontimeupdate event and check it if it is 5 then pause the current and start the second which should now be buffered and when it reaches to 0:35 pause it and start the first one and let it play till the audio finishes.
Play: 0:00 to 0.05, Skip: 0:05 to 0:30, Play: 0:30 to 0:35, Skip: 0:35
to 0:05, Play: 0.05 to END
You can use Media Fragments URI and pause event to render exact media playback in sequence. If necessary pass the HTMLMediaElement to AudioContext.createMediaElementSource()
const audio = document.querySelector("audio");
const mediaURL = "https://ia600305.us.archive.org/30/items/return_201605/return.mp3";
const timeSlices = ["#t=0,5", "#t=30,35", "#t=5"];
let index = 0;
audio.onpause = e => {
if (index < timeSlices.length) {
audio.src = `${mediaURL}${timeSlices[index++]}`
} else {
console.log("Media fragments playlist completed");
}
}
audio.ontimeupdate = e => console.log(Math.ceil(audio.currentTime));
audio.oncanplay = e => audio.play();
audio.src = `${mediaURL}${timeSlices[index++]}`;
<audio controls></audio>
When the user watches a video, I would like to make 2 AJAX calls. One when the user finished watching the video and the time played is equal or more than the duration of the video (because users can rewind as well then). timePlayed>=duration && event.type=="ended". I successfully make the call for that.
Where I struggle is that I would also like to make a call when the video is watched more than 80% and the time played of the video is more than 80% as well in order to prevent the user from just fast forwarding.
In order for that to work I have to modify my videoStartedPlaying() method and this is where I come across issues as I am trying to set an interval. Now, with setting an interval, it is like an endless loop.
var video_data = document.getElementById("video");
var timeStarted = -1;
var timePlayed = 0;
var duration = 0;
// If video metadata is loaded get duration
if(video_data.readyState > 0)
getDuration.call(video_data);
//If metadata not loaded, use event to get it
else {
video_data.addEventListener('loadedmetadata', getDuration);
}
// remember time user started the video
function videoStartedPlaying() {
timeStarted = new Date().getTime()/1000;
setInterval(function(){
playedFor = new Date().getTime()/1000 - timeStarted;
checkpoint = playedFor / duration;
percentComplete = video_data.currentTime/video_data.duration;
// here I need help of how to best accomplish this
if (percentComplete >= 0.8 && checkpoint >= 0.8) {
// AJAX call here
}
}, 2000);
}
function videoStoppedPlaying(event) {
// Start time less then zero means stop event was fired vidout start event
if(timeStarted>0) {
var playedFor = new Date().getTime()/1000 - timeStarted;
timeStarted = -1;
// add the new amount of seconds played
timePlayed+=playedFor;
}
// Count as complete only if end of video was reached
if(timePlayed>=duration && event.type=="ended") {
// AJAX call here
}
}
function getDuration() {
duration = video_data.duration;
}
video_data.addEventListener("play", videoStartedPlaying);
video_data.addEventListener("playing", videoStartedPlaying);
video_data.addEventListener("ended", videoStoppedPlaying);
video_data.addEventListener("pause", videoStoppedPlaying);
I truly would appreciate any help with this as it seems like I am at my wits end.
Thanks so much!
Edit:
Thanks to the comment I came up with this:
const video = document.getElementById("video");
const set = new Set();
const percent = .8;
let toWatch;
function mediaWatched (curr) {
alert(`${curr}% of media watched`)
}
function handleMetadata(e) {
toWatch = Math.ceil(video.duration * percent);
console.log(toWatch, video.duration);
}
function handleTimeupdate (e) {
set.add(Math.ceil(video.currentTime));
let watched = Array.from(set).pop();
if (set.has(toWatch) && watched === toWatch) {
video.removeEventListener("timeupdate", handleTimeupdate);
console.log(watched);
mediaWatched(
Math.round(watched / Math.ceil(video.duration) * 100)
);
}
}
video.addEventListener("loadedmetadata", handleMetadata);
video.addEventListener("timeupdate", handleTimeupdate);
<video width="400" height="300" controls="true" poster="" id="video">
<source type="video/mp4" src="http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_2mb.mp4" />
</video>
Now, for example, if I fast forward to around 50% length, and let it play then, it will fire whenever 80% of the movie is reached, but it shouldn't because I fast forwarded to 50% and essentially only watched 30%.
Does that make sense? How can I achieve such behavior?
per discussion in comments here's a working sample.
It includes a couple of handlers just to make life easier in setting up the array and summing the contents so you know when you have reached the 80% mark (though you may need to change that logic if you want to force them to, say, explicit watch the first 80% not just a total of 80% throughout the video).
There are a number of console.log(...) statements in there so you can watch what it's doing in the browser console window... you'll probably want to take them out before deploying for real.
I've put the hook for where to make the ajax call in the timeupdate event, but you could always use a regular setInterval timer in the main loop as well to check for the 80% and make the call there, but this seemed cleaner
most of it should be self explanatory, but do ask in comments if there's anything that's not clear...
<video controls preload="auto" id="video" width="640" height="365" muted>
<source src="http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_2mb.mp4" type="video/mp4">
</video>
<script>
// handler to let me resize the array once we know the length
Array.prototype.resize = function(newSize, defaultValue) {
while(newSize > this.length)
this.push(defaultValue);
this.length = newSize;
}
// function to round up a number
function roundUp(num, precision) {
return Math.ceil(num * precision) / precision
}
var vid = document.getElementById("video")
var duration = 0; // will hold length of the video in seconds
var watched = new Array(0);
var reported80percent = false;
vid.addEventListener('loadedmetadata', getDuration, false);
vid.addEventListener('timeupdate',timeupdate,false)
function timeupdate() {
currentTime = parseInt(vid.currentTime);
// set the current second to "1" to flag it as watched
watched[currentTime] = 1;
// show the array of seconds so you can track what has been watched
// you'll note that simply looping over the same few seconds never gets
// the user closer to the magic 80%...
console.log(watched);
// sum the value of the array (add up the "watched" seconds)
var sum = watched.reduce(function(acc, val) {return acc + val;}, 0);
// take your desired action on the ?80% completion
if ((sum >= (duration * .8)) && !reported80percent) {
// set reported80percent to true so that the action is triggered once and only once
// could also unregister the timeupdate event to avoid calling unneeded code at this point
// vid.removeEventListener('timeupdate',timeupdate)
reported80percent = true;
console.log("80% watched...")
// your ajax call to report progress could go here...
}
}
function getDuration() {
console.log("duration:" + vid.duration)
// get the duration in seconds, rounding up, to size the array
duration = parseInt(roundUp(vid.duration,1));
// resize the array, defaulting entries to zero
console.log("resizing arrary to " + duration + " seconds.");
watched.resize(duration,0)
}
</script>
I want to know the bitrate during video playback in jwplayer 6's auto mode. When the video starts, the selected value is "Auto". But unlike other values, the "Auto" value doesn't include bitrate or other parameters.
my default parameters:
primary: "flash",
autostart:"true"
I've read this post but it didn't help me.
Someone can help me?
I got a demo from #EthanJWPlayer. It's very clear demo..
And i'm simplify that code.
Firstly add this method on your jwplayer functions:
var bitrateList = [],
bandwidth = 0,
currentBitrate =0,
levels;
jwplayer().setup({
....
events:
onQualityChange: function(callback) {
levels = callback.levels;
render();
},
onQualityLevels: function(callback) {
levels = callback.levels;
render();
},
onMeta: function(event) {
if (event.metadata.bandwidth) {
var b = event.metadata.bandwidth;
var l = Number(event.metadata.currentLevel.substr(0, 1));
if (b != bandwidth) {
bandwidth = b;
currentBitrate = bitrateList[l - 1];
}
}
}
});
function render() {
bitrateList = [];
for (var i = 1; i < levels.length; i++) {
bitrateList.push(levels[i].bitrate);
}
}
and you can be give anywhere on your JavaScript code from "currentBitrate" variable.
for example:
sendStatistics(currentBitrate);
In addition to adaptive bitrate streaming (adaptive streaming - jw player auto mode), changed every second bitrate value depending on the current bandwidth. If you want get value of the bitrate, append above code and get currentBitrate value.
I'm trying to get my video (locally hosted, not streamed) to start after a certain time and stop after a certain duration. Someone here helped me out here with Javascript, but it's not working for me -- no effect on time of playback at all.
So, in my header, I've called the javascript like this:
<script src="Backend/build/Timer.js"></script>
And the actual javascript looks like this:
// JavaScript Document
var starttime = 2000; // start at 2 seconds
var endtime = 4000; // stop at 4 seconds
var video = document.getElementById('player1');
video.currentTime = starttime;
video.addEventListener("timeupdate", function() {
if (video.currentTime >= endtime) {
video.pause();
}
}, false);
Wrap your code into a function and invoke that function in document ready or body load event handler, otherwise video variable value may be invalid.
According to W3C standard:
The currentTime attribute must, on getting, return the current playback position, expressed in seconds. On setting, if the media element has a current media controller, then it must throw an INVALID_STATE_ERR exception; otherwise, the user agent must seek to the new value (which might raise an exception).
If you want to start playing the video at 2 seconds and stop at 4 seconds (in the video time stamp), set starttime, endtime to 2, 4 respectively, not 2000, 4000. Furthermore, before seeking to starttime, you must load the video resource once
function playVideo() {
var starttime = 2; // start at 2 seconds
var endtime = 4; // stop at 4 seconds
var video = document.getElementById('player1');
//handler should be bound first
video.addEventListener("timeupdate", function() {
if (this.currentTime >= endtime) {
this.pause();
}
}, false);
//suppose that video src has been already set properly
video.load();
video.play(); //must call this otherwise can't seek on some browsers, e.g. Firefox 4
try {
video.currentTime = starttime;
} catch (ex) {
//handle exceptions here
}
}