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

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.

Related

Adding Audio with JavaScript: FireFox and Chrome show they're playing, but I'm not receiving any sound

My peer and I have a series of JavaScript objects which all have an audio property. The value of that property is a string which provides the relative path to where that audio file is stored on our server.
I wrote a JavaScript function which references those objects and plays their audio files when a new object loads. Everything seemed to be working fine during testing. He just added new audio files today and messaged me that our project wasn't playing audio in IE -- but it still worked in FF and Chrome.
When I checked the error, I noticed IE was getting an invalid state error from the playAudio JavaScript function that I wrote. I made the assumption the mp3 file didn't fully load, so IE was throwing the invalid state error because it couldn't execute the play() command on a file that wasn't available yet. So, tonight I added a media event listener to ensure the function didn't execute until the mp3 resource was available.
That fixed the IE issue, but now both Chrome and FireFox aren't playing sound. And by that I mean, they are receiving the mp3 resource, no errors are being thrown to the console, and they both show the audio icon on the browser's tab above the url bar as though media content were actually playing. It seems like they are playing, but I'm not getting sound through my actual speakers. I'm kind of at a loss as to why this is happening, and will have to look into it more tomorrow -- but I'm hoping someone here might know what could be the cause.
// The playAudio function pulls the audio file from the scenes object and only plays once.
// Afterward, the scenes object receives a new parameter, audio_played, which is set to true.
function playAudio(id) {
// check whether the variable currAudio has been defined.
// If it has, pause currAudio.
if (typeof currAudio !== 'undefined') {
currAudio.pause();
}
// check that the scenes object has an audio parameter
// and that the audio_played parameter is not true
if (scenes[id].audio && scenes[id].audio_played !== true) {
// create a new Audio object using the loaded scene's audio
// parameter and store it in the variable newAudio
var newAudio = new Audio(scenes[id].audio);
// wait for the audio file to load enough data
// to be played, then execute the nested scripts.
newAudio.addEventListener('canplay', function() {
// set newAudio to start at the beginning of the file.
newAudio.currentTime = 0;
// play the audio file
newAudio.play();
// add an audio_played parameter to the
// scenes object and set it's value to true.
scenes[id].audio_played = true;
// update the currAudio variable
var currAudio = newAudio;
}, false);
}
}

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

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.

PhoneGap unable to getDuration() out of Media API, but other methods work

I'm building out an audio media recorder/player with PhoneGap. It's all working beautifully, but I've hit a wrinkle I can't seem to iron.
my_media.play(); does indeed play the media w/o error in my Eclipse or XCode consoles which is why the alert that is showing a -1 is puzzling. I expect my_media.getDuration(); to return the duration of the file I'm attempting to play.
My try/catch block isn't throwing an error, I'm quite puzzled on this one. Here's the PhoneGap documentation on Media.getDuration().
function playAudio() {
$('#btnStopRecording').removeClass('ui-disabled');
$('#btnPlayMessage, #btnStartStopRecording, #btnDeleteMessage, #btnAcceptUpload').addClass('ui-disabled');
my_media = new Media(fullRecordPath,
// success callback
function () {
$('#btnPlayMessage, #btnStartStopRecording, #btnDeleteMessage, #btnAcceptUpload').removeClass('ui-disabled');
$('#btnStopRecording').addClass('ui-disabled');
},
// error callback
function (err) {
console.log("attempting to play fullRecordPath = "+fullRecordPath);
console.log("playAudio():Audio Error: " + err.code);
}
);
var thisDuration;
try{
thisDuration = my_media.getDuration();
} catch (err) {
console.log("attempting to get duration error code "+err.code);
console.log("attempting to get duration error message "+err.message);
}
alert("we're about play a file of this duration "+thisDuration);
my_media.play();
// stop playback when the stop button is tapped
$('#btnStopRecording').off('tap').on('tap',function()
{
my_media.stop();
$('#btnPlayMessage, #btnStartStopRecording, #btnDeleteMessage, #btnAcceptUpload').removeClass('ui-disabled');
$('#btnStopRecording').addClass('ui-disabled');
});
// if the user leaves the page, stop playback
$('#pageRecordMessage').live('pagehide', function()
{
my_media.stop();
$('#btnPlayMessage, #btnStartStopRecording, #btnDeleteMessage, #btnAcceptUpload').removeClass('ui-disabled');
$('#btnStopRecording').addClass('ui-disabled');
});
}
The metadata for the media in question has not been loaded when you call my_media.getDuration(). In the documentation you referenced in your question the example code puts the getDuration call into an interval:
var timerDur = setInterval(function() {
counter = counter + 100;
if (counter > 2000) {
clearInterval(timerDur);
}
var dur = my_media.getDuration();
if (dur > 0) {
clearInterval(timerDur);
document.getElementById('audio_duration').innerHTML = (dur) + " sec";
}
}, 100);
I would recommend doing something similar.
This solution works for me. Basically, play and immediately stop. It doesn't seem to take any time, seems like a decent workaround.
media.play();
media.stop();
var length = media.getDuration();
This question is too old. But it is still relevant because many might have been facing this same problem.
Whenever nothing works I just do one thing, upgrade or downgrade the version. In this case I solved my problem by installing following version.
cordova plugin add cordova-plugin-media#1.0.1
I also faced similar problem in cordova for iOS. I was successfully able to record, play and stop audio but unable to get current position and total duration for audio file. So I added my_media.release() just after I was done finishing recording audio i.e. after my_media.stopRecord() and it worked like a charm. Earlier I was getting -1 for getDuration() and 0 for getCurrentPosition().
Hope it helps someone.

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