PhoneGap Media Plugin - can you detect when url stream has loaded? - javascript

I been having some issues getting my audio player to work correctly in iOS and Android using the PhoneGap Media plugin through Build 3.1
I have play and stop buttons that work fine but when you hit play there is a slight delay as the audio url loads and it freezes the OS. I can kind of cope with that as its a short delay so I thought I'd pop up a loading icon onscreen.
Ive posted a few time here trying to get the JS to work as im no expert but just now I've realised that there is nothing in my code to check if the audio is actually PLAYING!
The playAudio function loads in the audio and then sets the play button to a stop button. I thought function success() {meant it was playing but it actually means it FINISHED playing.
In my code when the link to the audio is clicked the loader function makes a spinner appear on screen and I thought 'success' would turn the loader off because its loaded when in fact whats happening is the laoder stays on and goes away after the track has finished!
I've realized I need a way of detecting if the track is actually PLAYING!
How do I do that?!?!!
Heres the function:
function loadPlay(src, trackName) {
loader();
playAudio(src, trackName);
}
function loader() {
// make a loader spinner appear as track loads
$(".loading").addClass("loadingnow");
}
function playAudio(src,trackname) {
// for Android
if (audioPlaying === false) {
if (device.platform == 'Android') {
src = '/android_asset/www/' + src;
}
media = new Media(src, success, error_error);
media.play();
//add playing class so play button is replaced with stop button
document.getElementById(trackname).parentNode.className="playing";
audioPlaying = true;
} else {
//audio is already playing
}
}
function success() {
// track isplaying so remove the stop button put play button back
$(".playing").removeClass("playing");
// now track is playing remove the loader spinner
$(".loading").removeClass("loadingnow");
audioPlaying = false;
}
function error_error(e) {
//alert('great error');
//alert(e.message);
}
function stopAudio() {
// stop playing track
if (media) {
media.stop();
audioPlaying = false;
}
}
Heres a link to the PhoneGap Media plugin API:
http://docs.phonegap.com/en/3.1.0/cordova_media_media.md.html#Media
Do I need to use this bit?
Media.MEDIA_STARTING = 1;
Media.MEDIA_RUNNING = 2;
Media.MEDIA_PAUSED = 3;
Media.MEDIA_STOPPED = 4;

From the docs:
The following constants are reported as the only parameter to the mediaStatus callback function.
Media.MEDIA_NONE = 0;
Media.MEDIA_STARTING = 1;
Media.MEDIA_RUNNING = 2;
Media.MEDIA_PAUSED = 3;
Media.MEDIA_STOPPED = 4;
The mediaStatus callback is an optional 3rd callback supplied in your Media constructor...
media = new Media(src, success, error_error, status_change);
function status_change(code) {
switch (code) :
case Media.MEDIA_STOPPED : doSomething(); break;
}

From what I can tell, ilovett's answer might not work for you, because the doSomething() function will get called in two other cases:
When someone stops the music with a media.stop()
When the media gets released (media.release()) on android (not sure about others, but iOS wont call it).
the way I'm doing it is setting a variable to make sure that the function is not getting called accidentally.

Related

Playing audio with javascript

I am trying to play a beep sound a minute after user has come on the page of my website. I found the solution here https://stackoverflow.com/a/18628124/912359
Here's my code:
$(document).ready(function(){
setTimeout(function () {
try{
if(!$(".facebook-chat").hasClass("active")){
$(".facebook-chat").addClass("active");
var audio = new Audio("/sound/chat.mp3");
audio.play();
}
}catch(e){
}
}, 60000);
}):
This throws an exception:
Uncaught (in promise) DOMException
Strangely, once I load the sound file separately in my browser and come back to the page, it works perfectly. Any ideas how I can fix it.
[Edit]
The issue is that user has to interact with the browser before the sound can be played. So I put the same code under click event of the body and it works. But the same doesn't work on scroll event either. I guess chrome doesn't consider scroll a user interaction. Can anyone add what other interactions can be used to trigger this?
Also, how is it working if I load the audio file in a separate window and come back to my page.
You can try loading the audio when the document is ready and then play it later only if the resource is loaded (for this check you can register a callback on onloadeddata). Otherwise, if resource is not loaded, you can try loading it again.
$(document).ready(function()
{
let aud = new Audio('https://dl.dropboxusercontent.com/s/1cdwpm3gca9mlo0/kick.mp3');
let canPlay = false;
aud.onloadeddata = () => (console.log("audio loaded"), canPlay = true);
setInterval(function()
{
if (canPlay)
aud.play();
else
aud = new Audio('https://dl.dropboxusercontent.com/s/1cdwpm3gca9mlo0/kick.mp3');
}, 3000);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Best solution I could come up with when I tried the same was:
const playPromise = audio.play();
if (playPromise !== null){
playPromise.catch(function() { audio.play(); })
}
But sometimes (One out of ten) the second audio.play() where also uncaught and the audio did not play either. I suggest you made a loop that stops only when the Promise is finally caught.

Reverse an animation when vimeo video is done playing

I have a video module that uses a splash screen and on click, reveals a full screen video for screen sizes 667 +. I would like to have this reverse it's animation after ending the video so it returns to the splash screen. I'm not really sure where to even start or whether this is possible. Any help is appreciated!
$(function(){
var $parent = $('.video-hero'),
$video = $parent.find('iframe'),
$playButton = $(".play"),
$itemsToFadeOut = $(".vid-cap, .ghost"),
f = $video[0],
url = $video.attr('src').split('?')[0],
activeVideoClass = "video-started";
// setup fitVids
$parent.fitVids();
// handle play click
$playButton.click(function(e){
e.preventDefault();
// grab height of video
var videoHeight = $video.height();
// add class to hero when video is triggered
$parent.addClass(activeVideoClass);
// fade out the play button
$(this).fadeOut("fast");
// fade out poster image, overlay, and heading
$itemsToFadeOut.fadeOut();
// toggle accessibility features
$video.attr({
"aria-hidden" : "false",
"tabindex" : "0"
});
// set focus to video for accessibility control
$video.focus();
// set height of hero based on height of video
$parent.css("max-height",videoHeight).height(videoHeight);
// send play command to Vimeo api
runCommand('play');
});
// send play to vimeo api
var runCommand = function(cmd){
var data = {method : cmd};
f.contentWindow.postMessage(JSON.stringify(data), url);
}
// handle resize
$(window).resize(function(){
var videoHeight = $video.height();
if($(".video-started").size() === 1){
$parent.height(videoHeight);
}
});
});
Remember to resize my JSFiddle so you're able to see the animation I am talking about.
Figured it out everyone! I'll explain each chunk of code step by step for anyone's future reference.
I was able to accomplish what I needed to do without the froogaloop cdn, and just using fitvids.js here is a working fiddle of my solutions.
I listed all of my JS below in sections, but for the answer to my question of "reversing my function after video finishes" you will only need to pay attention to my Event Handlers, Connection to the API, and Player State Functions. Once I was able to make the connection and read that the video was ended, I used addClass(); and removeClass(); coupled with CSS Transitions to handle swapping between my play and ready(post finish) states.
I tried to document and explain as much as I could so hopefully this can help someone in the future!
Naming my Vars
Not much to mention here, this is just the preliminary, the main thing to pay attention to is var url it's the only way I could write it to allow me to use my listeners with the Vimeo api.
var parent = $('.video-hero'),
f = $('iframe'),
$playButton = $(".play"),
$itemsToFadeOut = $(".vid-cap, .ghost, .play"),
$video = f[0],
url = f.attr('src').split('?')[0],
activeVideoClass = "video-started", //Class for when video is playing
standardClass = "standard"; //Class for when video is finished/before play
Event Listeners / Handlers
My listeners just wait to receive a message from the api/player of whether the video is ready, paused, finished or playing.
listeners
// Listen for messages from the player
if (window.addEventListener){
window.addEventListener('message', onMessageReceived, false);
}
else {
window.attachEvent('onmessage', onMessageReceived, false);
}
My handlers well... handle when my functions are fired. My functions are located below, this just lets me choose which state (case) I have being sent from the API and how to react to it.
handlers
// Handle messages received from the player
function onMessageReceived(e) {
var data = JSON.parse(e.data);
switch (data.event) {
//Ready case is before play / after finish
case 'ready':
onReady();
break;
case 'pause':
onPause();
break;
case 'finish':
onFinish();
break;
}
}
Connecting to the Vimeo API
This section communicates hand in hand with my html play button and vimeo's api/player, allowing me to run, pause and stop the video
// send play to vimeo api
var runCommand = function(cmd){
var data = {method : cmd};
f[0].contentWindow.postMessage(JSON.stringify(data), url);
}
// Helper function for sending a message to the player
function post(action, value) {
var data = { method: action };
if (value) {
data.value = value;
}
f[0].contentWindow.postMessage(JSON.stringify(data), url);
}
Player State Functions
What will happen based on which state or case the player is in
function onReady() {
post('addEventListener', 'finish');
}
function onPause() {
console.log('paused');
}
function onFinish() {
// add class to hero when video is triggered
parent.removeClass(activeVideoClass);
parent.addClass(standardClass);
// fade out the play button
$(this).fadeIn("slow");
// fade out poster image, overlay, and heading
$itemsToFadeOut.fadeIn();
}
To detect the end of the video, and run JS code, you might need to make use of the Froogaloop library:
https://developer.vimeo.com/player/js-api#universal-with-froogaloop
You can then do something like:
var player = $f(iframe[0]);
player.addEvent('ready', function() {
player.addEvent('finish', function() {
// Animation...
});
});
The different events are here:
https://developer.vimeo.com/player/js-api#events
I do this on a site I built recently in order to close the modal window on video end / finish:
http://billy.fm/
Feel free to examine the JS code there (unminified version):
http://billy.fm/wp-content/themes/billy/js/main.js
Wish I had more time to help you, but this should set you off on the right track.

PhoneGap on iOS: play new media before currently playing one ends

I have a simple JavaScript game built on PhoneGap (Cordova), where user is looking for certain features in a picture. After a correct feature if found, an audio (jingle notifying of correct answer) is played. Though I have a problem - the jingle is X seconds long. When the user finds another correct feature before the previous jingle finished playing (time is smaller than X), the new jingle will not start playing. I need every new jingle to override the previous one in case the previous one has not finished playing yet (and so let the user acoustically the last correct feature guess).
Here is my code - it working like I want on Android:
var gMedia;
function playAudio(src, callback) {
if (gMedia) {
gMedia.stop();
gMedia.release();
}
var mediaSource;
if (iOS) mediaSource = "sounds/" + src;
else mediaSource = "/android_asset/www/sounds/" + src;
gMedia = new Media(mediaSource, onSuccess, onError);
gMedia.play();
gMedia.setVolume('1.0');
// onSuccess Callback
function onSuccess() {
gMedia.release();
if (callback && typeof(callback) === "function") {
callback.call();
}
}
// onError Callback
function onError(error) {
gMedia.release();
}
}
It seems, the problem is that you release the Media object in your success callback because it will also be called when you stop the sound (gMedia.stop()). So when you create the new Media object it will be released right away.
In the tests I've made your code works fine after removing the gMedia.release() from your onSuccess function.

Playing HTML5 Video on IPad and seeking

Very strange bug I can't seems to figure out.
I am trying to get an HTML5 video to play from a certain position when a user hits play. I am trying to have it seek right when the video starts to play.
On my play event I do this.currentTime = X
On the browser it works fine. But on the IPad, when I play the video, the video doesn't seek to the right position (it starts from zero).
Even more oddly, if I do the this.currentTime = X call in a setTimeout of let's say 1 second, it works on the IPad (sometimes).
On iOS, videos load at play time (see item #2), not at page load time. My guess is that the video is not loaded when you run this.currentTime = X, so it has no effect. This also explains why delaying the operation can sometimes fix the problem: sometimes it has loaded after a second, sometimes not.
I don't have an iOS device to test, but I'd suggest binding a loadeddata listener to the video so that your currentTime manipulation only happens after the video begins loading:
// within the play event handler...
if(!isIOSDevice) {
this.currentTime = X;
} else {
function trackTo(evt) {
evt.target.currentTime = X;
evt.target.removeEventListener("loadeddata", trackTo)
});
this.addEventListener("loadeddata", trackTo);
}
You'll need to set isIOSDevice elsewhere in your code, based on whether the current visit comes from an iOS device.
While this question is quite old the issue remains open. I discovered a solution that works on multiple tested iPads (1+2+3) for iOS 5 and 6 (iOS3+4 not tested):
Basically you first have to wait for the initial playing event, then add a one-time binder for canplaythrough and then for progress - only then can you actually change the the currentTime value. Any tries before that will fail!
The video has to start playing at first, which makes a black layer on top of the video element kinda handy. Unfortunately, sounds within the video canNOT be deactivated via JavaScript --> not a perfect UX
// https://github.com/JoernBerkefeld/iOSvideoSeekOnLoad / MIT License
// requires jQuery 1.8+
// seekToInitially (float) : video-time in seconds
function loadingSeek(seekToInitially, callback) {
if("undefined"==typeof callback) {
callback = function() {};
}
var video = $("video"),
video0 = video[0],
isiOS = navigator.userAgent.match(/(iPad|iPhone|iPod)/) !== null,
test;
if(isiOS) { // get the iOS Version
test =navigator.userAgent.match("OS ([0-9]{1})_([0-9]{1})");
// you could add a loading spinner and a black layer onPlay HERE to hide the video as it starts at 0:00 before seeking
// don't add it before or ppl will not click on the play button, thinking the player still needs to load
}
video.one("playing",function() {
if(seekToInitially > 0) {
//log("seekToInitially: "+seekToInitially);
if(isiOS) {
// iOS devices fire an error if currentTime is set before the video started playing
// this will only set the time on the first timeupdate after canplaythrough... everything else fails
video.one("canplaythrough",function() {
video.one("progress",function() {
video0.currentTime = seekToInitially;
video.one("seeked",function() {
// hide the loading spinner and the black layer HERE if you added one before
// optionally execute a callback function once seeking is done
callback();
});
});
});
} else {
// seek directly after play was executed for all other devices
video0.currentTime = seekToInitially;
// optionally execute a callback function once seeking is done
callback();
}
} else {
// seek not necessary
// optionally execute a callback function once seeking is done
callback();
}
});
}
the whole thing can be downloaded from my GitHub repo
apsillers is right. Once the video starts playing, the Quicktime player will come up and the video will not be seekable until the first 'progress' event is triggered. If you try to seek before then, you'll get an invalid state error. Here's my code:
cueVideo = function (video, pos) {
try {
video.currentTime = pos;
// Mobile Safari's quicktime player will error if this doesn't work.
} catch(error) {
if (error.code === 11) { // Invalid State Error
// once 'progress' happens, the video will be seekable.
$(video).one('progress', cueVideo.bind(this, video, pos));
}
}
}
Appreciate the attempts for answers below. Unfortunately, had to resort to just checking inside timeupdate if the currenttime was > 0 and < 1, if it was then went to that part of the video and removed the listener to timeupdate.
try to limit your X to 1 decimal
X.toFixed(1);
As you mentioned it works sometimes after a time of 1 second. Have you tried to set the position after the playing event fires? or maybe even the canplaythrough event
Take a look at the source of this page to see a whole list of events that can be used (in the javascript file)

Problems preloading audio in Javascript

I'm trying to make a cross-device/browser image and audio preloading scheme for a GameAPI I'm working on. An audio file will preload, and issue a callback once it completes.
The problem is, audio will not start to load on slow page loads, but will usually work on the second try, probably because it cached it and knows it exists.
I've narrowed it down to the audio.load() function. Getting rid of it solves the problem, but interestingly, my motorola droid needs that function.
What are some experiences you've had with HTML5 audio preloading?
Here's my code. Yes, I know loading images in a separate function could cause a race condition :)
var resourcesLoading = 0;
function loadImage(imgSrc) {
//alert("Starting to load an image");
resourcesLoading++;
var image = new Image();
image.src = imgSrc;
image.onload = function() {
//CODE GOES HERE
//alert("A image has been loaded");
resourcesLoading--;
onResourceLoad();
}
}
function loadSound(soundSrc) {
//alert("Starting to load a sound");
resourcesLoading++;
var loaded = false;
//var soundFile = document.createElement("audio");
var soundFile = document.createElement("audio");
console.log(soundFile);
soundFile.autoplay = false;
soundFile.preload = false;
var src = document.createElement("source");
src.src = soundSrc + ".mp3";
soundFile.appendChild(src);
function onLoad() {
loaded = true;
soundFile.removeEventListener("canplaythrough", onLoad, true);
soundFile.removeEventListener("error", onError, true);
//CODE GOES HERE
//alert("A sound has been loaded");
resourcesLoading--;
onResourceLoad();
}
//Attempt to reload the resource 5 times
var retrys = 4;
function onError(e) {
retrys--;
if(retrys > 0) {
soundFile.load();
} else {
loaded = true;
soundFile.removeEventListener("canplaythrough", onLoad, true);
soundFile.removeEventListener("error", onError, true);
alert("A sound has failed to loaded");
resourcesLoading--;
onResourceLoad();
}
}
soundFile.addEventListener("canplaythrough", onLoad, true);
soundFile.addEventListener("error", onError, true);
}
function onResourceLoad() {
if(resourcesLoading == 0)
onLoaded();
}
It's hard to diagnose the problem because it shows no errors and only fails occasionally.
I got it working. The solution was fairly simple actually:
Basically, it works like this:
channel.load();
channel.volume = 0.00000001;
channel.play();
If it isn't obvious, the load function tells browsers and devices that support it to start loading, and then the sound immediately tries to play with the volume virtually at zero. So, if the load function isn't enough, the fact that the sound 'needs' to be played is enough to trigger a load on all the devices I tested.
The load function may actually be redundant now, but based off the inconsistiency with audio implementation, it probably doesn't hurt to have it.
Edit: After testing this on Opera, Safari, Firefox, and Chrome, it looks like setting the volume to 0 will still preload the resource.
canplaythrough fires when enough data has buffered that it probably could play non-stop to the end if you started playing on that event. The HTML Audio element is designed for streaming, so the file may not have completely finished downloading by the time this event fires.
Contrast this to images which only fire their event once they are completely downloaded.
If you navigate away from the page and the audio has not finished completely downloading, the browser probably doesn't cache it at all. However, if it has finished completely downloading, it probably gets cached, which explains the behavior you've seen.
I'd recommend the HTML5 AppCache to make sure the images and audio are certainly cached.
The AppCache, as suggested above, might be your only solution to keep the audio cached from one browser-session to another (that's not what you asked for, right?). but keep in mind the limited amount of space, some browsers offer. Safari for instance allows the user to change this value in the settings but the default is 5MB - hardly enough to save a bunch of songs, especially if other websites that are frequented by your users use AppCache as well. Also IE <10 does not support AppCache.
Alright so I ran into the same problem recently, and my trick was to use a simple ajax request to load the file entirely once (which end into the cache), and then by loading the sound again directly from the cache and use the event binding canplaythrough.
Using Buzz.js as my HTML5 audio library, my code is basically something like that:
var self = this;
$.get(this.file_name+".mp3", function(data) {
self.sound = new buzz.sound(self.file_name, {formats: [ "mp3" ], preload: true});
self.sound.bind("error", function(e) {
console.log("Music Error: " + this.getErrorMessage());
});
self.sound.decreaseVolume(20);
self.sound.bind("canplaythrough",function(){ self.onSoundLoaded(self); });
});

Categories

Resources