With the release of OSX High-Sierra*, one of the new features in Safari is that videos on websites will not auto play anymore and scripts can't start it either, just like on iOS. As a user, I like the feature, but as a developer it puts a problem before me: I have an in-browser HTML5 game that contains video. The videos do not get automatically played anymore unless the user changes their settings. This messes up the game flow.
My question is, can I somehow use the players' interaction with the game as a trigger for the video to start playing automatically, even if said activity is not directly linked to the video element?
I cannot use jQuery or other frameworks, because of a restraint that my employer has put on our development. The one exception is pixi.js which - among all other animations - we are also using to play our videos inside a pixi container.
*The same restriction also applies on Mobile Chrome.
Yes, you can bind on event that are not directly ones triggered on the video element:
btn.onclick = e => vid.play();
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
So you can replace this button with any other splash screen requesting an user click, and you'll be granted access to play the video.
But to keep this ability, you must call at least once the video's play method inside the event handler itself.
Not working:
btn.onclick = e => {
// won't work, we're not in the event handler anymore
setTimeout(()=> vid.play().catch(console.error), 5000);
}
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
Proper fix:
btn.onclick = e => {
vid.play().then(()=>vid.pause()); // grants full access to the video
setTimeout(()=> vid.play().catch(console.error), 5000);
}
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
Ps: here is the list of trusted events as defined by the specs, I'm not sure if Safari limits itself to these, nor if it includes all of these.
Important note regarding Chrome and preparing multiple MediaElements
Chrome has a long-standing bug caused by the maximum simultaneous requests per host which does affect MediaElement playing in the page, limiting their number to 6.
This means that you can not use the method above to prepare more than 6 different MediaElements in your page.
At least two workarounds exist though:
It seems that once a MediaElement has been marked as user-approved, it will keep this state, even though you change its src. So you could prepare a maximum of MediaElements and then change their src when needed.
The Web Audio API, while also concerned by this user-gesture requirement can play any number of audio sources once allowed. So, thanks to the decodeAudioData() method, one could load all their audio resources as AudioBuffers, and even audio resources from videos medias, which images stream could just be displayed in a muted <video> element in parallel of the AudioBuffer.
In my case i was combining transparent video (with audio) with GSAP animation. The solution from Kaiido works perfectly!
First, on user interaction, start and pause the video:
videoPlayer.play().then(() => videoPlayer.pause());
After that you can play it whenever you want. Like this:
const tl = gsap.timeline();
tl.from('.element', {scale: 0, duration: 5);
tl.add(() => videoPlayer.play());
Video will play after the scale animation :).
Tested in Chrome, Safari on iPhone
Related
I'm making a game made of bare JavaScript code and I want to manipulate audio in JavaScript.
Specifically, I want to let the page start playing music when some div element is clicked and when using some function in the JavaScript code.
I know audio tag of HTML5 has a limitation that a music file associated with Audio element cannot be played without user clicking the play button of the audio element, so I cannot do like this:
const audio = document.createElement("audio");
audio.src = "url of source";
audio.play();
//Uncaught (in promise) DOMException: play() failed because
//the user didn't interact with the document first.
//https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
Chrome's autoplay policies are simple:
Muted autoplay is always allowed.
Autoplay with sound is allowed if:
User has interacted with the domain (click, tap, etc.).
On desktop, the user's Media Engagement Index threshold has been crossed, meaning the user has previously played video with sound.
The user has added the site to their home screen on mobile or installed the PWA on desktop.
Top frames can delegate autoplay permission to their iframes to allow autoplay with sound.
But I want to manage audio more freely in order to make a good UI of the game.
To achieve my goal, I think I need to introduce another library or API into the JavaScript code or still use audio tag with some tricky tips.
I'm thinking one of the choices could be Web Audio API.
Could you tell me some advice?
Progress
I tried code like the following and it worked when I clicked a div element with click event handler:
<div id="my-div">play</div>
<audio id="myAudio">
<source src="url of source" type="audio/mpeg">
</audio>
const myAudio = document.getElementById("myAudio");
const btn = document.getElementById("my-div");
btn.addEventListener("click",() => {
myAudio.play();
});
However, when I wrote like the following, it didn't work with the same error even if it didn't seem I broke the autoplay policy:
document.getElementById('my-div').addEventListener('click', () => {
const audio = new Audio('url of source');
audio.play();
});
//Uncaught (in promise) DOMException: play() failed because
//the user didn't interact with the document first.
//https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
The main difference between two codes above is whether I wrote the url of the audio source file in the audio tag or wrote it in the source tag inside the audio tag. I'm guessing this makes some difference but I don't get what exactly is the problem.
You're correct. You can only be sure that the playback will not be blocked by the autoplay policy if the playback is started in response to a user gesture. A button with a click handler is the classic example but the click handler could be attached to a div as well. Let's say your div has an id called my-div. In that case the following should work.
document.getElementById('my-div').addEventListener('click', () => {
const audio = new Audio('url of source');
audio.play();
});
Using the Web Audio API will not change the autoplay policy. Usually the same rules apply no matter which browser API is used.
With the release of OSX High-Sierra*, one of the new features in Safari is that videos on websites will not auto play anymore and scripts can't start it either, just like on iOS. As a user, I like the feature, but as a developer it puts a problem before me: I have an in-browser HTML5 game that contains video. The videos do not get automatically played anymore unless the user changes their settings. This messes up the game flow.
My question is, can I somehow use the players' interaction with the game as a trigger for the video to start playing automatically, even if said activity is not directly linked to the video element?
I cannot use jQuery or other frameworks, because of a restraint that my employer has put on our development. The one exception is pixi.js which - among all other animations - we are also using to play our videos inside a pixi container.
*The same restriction also applies on Mobile Chrome.
Yes, you can bind on event that are not directly ones triggered on the video element:
btn.onclick = e => vid.play();
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
So you can replace this button with any other splash screen requesting an user click, and you'll be granted access to play the video.
But to keep this ability, you must call at least once the video's play method inside the event handler itself.
Not working:
btn.onclick = e => {
// won't work, we're not in the event handler anymore
setTimeout(()=> vid.play().catch(console.error), 5000);
}
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
Proper fix:
btn.onclick = e => {
vid.play().then(()=>vid.pause()); // grants full access to the video
setTimeout(()=> vid.play().catch(console.error), 5000);
}
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
Ps: here is the list of trusted events as defined by the specs, I'm not sure if Safari limits itself to these, nor if it includes all of these.
Important note regarding Chrome and preparing multiple MediaElements
Chrome has a long-standing bug caused by the maximum simultaneous requests per host which does affect MediaElement playing in the page, limiting their number to 6.
This means that you can not use the method above to prepare more than 6 different MediaElements in your page.
At least two workarounds exist though:
It seems that once a MediaElement has been marked as user-approved, it will keep this state, even though you change its src. So you could prepare a maximum of MediaElements and then change their src when needed.
The Web Audio API, while also concerned by this user-gesture requirement can play any number of audio sources once allowed. So, thanks to the decodeAudioData() method, one could load all their audio resources as AudioBuffers, and even audio resources from videos medias, which images stream could just be displayed in a muted <video> element in parallel of the AudioBuffer.
In my case i was combining transparent video (with audio) with GSAP animation. The solution from Kaiido works perfectly!
First, on user interaction, start and pause the video:
videoPlayer.play().then(() => videoPlayer.pause());
After that you can play it whenever you want. Like this:
const tl = gsap.timeline();
tl.from('.element', {scale: 0, duration: 5);
tl.add(() => videoPlayer.play());
Video will play after the scale animation :).
Tested in Chrome, Safari on iPhone
Is it possible to fully buffer HTML5 video in Chrome (and Opera)?
I host the movie in .mp4 and .webm on amazon S3. In HTML I use standard <video> tag. The server responds with status 206 Partial Content. It is great, as it allows the browser to download any part of the video but I need to be able to seek instantly to any part of the file.
I tried:
.PROGRESS event. When Chrome stops buffering the first part, the connection is killed. The next parts are downloaded in new connection, so JavaScript isn't able to continue checking total downloaded data. Even if it could, the old data isn't stored in cache. This method works in Firefox and Safari. Heck, even in IE10!
XHRS. I am able to fully download the movie, but when the video starts playing, Chrome tries to download the movie from the server again. I even tried to pass the movie into HTML in base64, but that's to much data
FileAPI. Chrome isn't able to create BLOB big enough and crashes.
Any help is welcome!
ps. I am aware of od and similar questions, but I hope something changed since they were asked.
Because S3 supports partial downloads, Chrome initially buffers "only what's needed" in front of the playhead and then stops buffering. It will continue buffering "only what's needed" when the video starts playing. This means that depending on the user's download speed it will stop buffering now and then, just because it has enough buffer to play continuously.
But if you pause the video after having played some, Chrome will not stop buffering and go through all the way to the end.
This example exploits that technique to entirely buffer the video off screen before showing it on the page.
Tested on Chrome 32
// Create video in background and start playing
var video = document.createElement('video');
video.src = 'video.mp4';
video.controls = true;
video.muted = true;
video.play();
// Pause immediately after it starts playing.
video.addEventListener("timeupdate", function() {
if (this.currentTime > 0) {
this.pause();
video.muted = false;
video.currentTime = 0
this.removeEventListener("timeupdate", arguments.callee, false);
// When whole video is buffered, show video on page
video.addEventListener("progress", function() {
if (Math.round(video.buffered.end(0)) / Math.round(video.seekable.end(0)) == 1) {
document.body.appendChild(video);
this.removeEventListener("progress", arguments.callee, false);
}
}, false);
}
}, false);
Have you tried the canplaythrough event?
Not in the traditional sense, I mean. Rather in a 'hacky' way. canplaythrough is triggered when the browser detects that it has enough video buffered to play continuously without pausing. I am guessing that this event triggers about the same time as chrome pauses buffering. If that's the case, it could be use to trigger a request for rest of the video.
I have not tried it, but the HTML5 video object contains a buffered property
var video = document.createElement('video');
video.buffered.start(0);
video.buffered.end(0);
Could you try monitoring the buffered (Such as with this thread chrome html5 video buffered.end event)
Then continually change the current time to right before the buffered time using
video.currentTime = video.buffered.end(0) - 1;
There are several ways to load a video all the way then play it:
1. There is the goody goody browser developer would be proud way:
var video = document.createElement('video');
video.src='myvideo.mp4';
document.appendChild(video);
video.load();
video.addEventListener('loadedmeta',function(){
video.play()
});
2. And there is the lazy "But, I'll get carpel tunnel!" developer way:
<video src='myvideo.mp4' onloadedmeta='this.play()' preload></video>
Sources:
http://www.w3schools.com/tags/tag_video.asp
how to run js code when video is fully buffered/loaded
How do I add new Video entirely from JavaScript?
javascript with html <video> load() & play() function not firing
http://www.w3.org/wiki/HTML/Elements/video
Be careful using video.buffered.end(0). According to my experience, it works with Chrome but it fails with IE9.
I don't know what happens, IE9 seems to forget to do the last update to video.buffered.end() at the end of the buffering process and the video.buffered.end(0) is a little bit smaller than video.duration.
So, if you think your page could be used with another browser than Chrome, don't use video.buffered.end(0).
Part of my current project involves loading external videos through HTML5's native video tag and then resizing them with Javascript to be the full height & width of the DOM.
My code seems to work perfectly on desktop browsers, but when I load up my project on my ipad the video doesn't get resized because the onloadedmetadata event never gets fired.
Here is a small code sample which reproduces the problem:
function init() {
var video = document.getElementById('viddy');
video.addEventListener('loadedmetadata', function(e){
var dimensions = [video.videoWidth, video.videoHeight];
alert(dimensions);
});
}
document.addEventListener("DOMContentLoaded", init, false);
<video id="viddy" autoplay>
<source src="http://media.w3.org/2010/05/sintel/trailer.webm" type='video/webm' />
<source src="http://www.w3schools.com/html/movie.mp4" type="video/mp4" />
</video>
http://jsfiddle.net/AUSNu/213/
I've even tried coding up a solution using jQuery, on the off-chance that the event may fire, but it still doesn't.
$('#viddy').on('loadedmetadata', function() {
alert('test');
});
I even went as far as enabling remote debugging through safari on my ipad, but still no output within the console.
Are there any workarounds to this? I couldn't find much info about this on the web / in documentation.
Unfortunately, there isn't really a way around this. Mobile Safari will not download any part of the video file until it gets a user interaction (i.e. some kind of touch event), not even the header, which is required to know the dimensions.
In your specific example, you need to enable controls on the video so the user can start it playing. (Or you can write your own code that starts it, but it has to be triggered by a touch or click event.) Once it starts playing, the loadedmetadata even will fire, and you can do what you want.
I recommend reading this other answer where someone else was trying to do pretty much the same thing. It discusses the problem in more detail along with a working link. Also, it addresses another problem with scaling the video that you will probably run into.
Safari on iPad (iOS6) does not scale HTML5 video to fill 100% of page width
let's get down to the code:
I'm preparing the document with:
<video width="300" height="400" id="videoStage"></video><button onclick='video()'>Play video</button>
as for the JS function:
function video(){
var vid_url = "https://fbcdn-video-a.akamaihd.net/cfs-ak-ash4/v/82342/969/274169121921_63622.mp4?oh=99ef0d9285cbbd7adf8bc07a845dc0d1&oe=519E400F&__gda__=1369439362_83c7f900e92bdbaa201f49d35a7c144a";
stage = document.getElementById('videoStage');
stage.src = vid_url;
stage.play();
}
working code sample: http://jsbin.com/eviyel/1
this code works just fine and plays the video both on my desktop and my mobile device.
But(!) when the video() function is applied to document.ready - it only plays on my desktop and as for my mobile device it displays empty.
Any solution guys? 10x.
I found the answer myself:
Mobile browsers (such as chrome mobile and iOs's) must have a valid 'click' first to enable both video+audio playback.
Once that click as been achieved, the element that was used to play the video/audio is now playable.
You can now change the SRC of that element to play any new media source programmatically without having to click on the DOM once again.
Starting from iOS 10 autoplaying videos that are muted is allowed without user interaction:
https://webkit.org/blog/6784/new-video-policies-for-ios/
Android still seems to require user interaction, although it might be possible to use a touchend event from scrolling the page to start the video playback.