DM Blog

HTML5 Audio with jQuery

by

Update (): this article refers to an older version of DanielMenjivar.com from 2010. A new version went live on so some of the code samples below aren’t in use on the new site anymore – you can still learn a lot from these examples though – it’s just not exactly the same as what’s currently on the site.

DanielMenjivar.com – New Design

In my last "techie" article about DanielMenjivar.com I went over some of the technology behind my music site and my decisions for choosing one over the other. Now it’s time to get deeper into things; I’ve decided to work my way backwards…

In this article, I’ll go over the JavaScript used on the site and as you’ve gathered from the title, I’ll focus on HTML5 audio with jQuery. But first, let’s get all the other JavaScript out of the way.

All the code examples below use jQuery, which makes life much easier.

Opening External Links in a New Window/Tab

// Open External Links in a new tab
$('a').live('click', function() {
	if ($(this).attr('rel') == 'external') { $(this).attr('target','_blank'); }
});

Pretty straight forward. All external links have rel="external" and this forces them to open in a new window/tab. I’m using the .live(); method to make sure that this also applies to any links loaded later on through AJAX.

Collapsable Content

All <article>s on the site (used on the audio, videos, chart samples and free charts pages) are expanded/open by default. Then JavaScript collapses/closes them upon page load. This way, if people have JavaScript turned off for whatever reason, all the page content is still accessible.

// Make article heights smaller (closed) upon loading
$('article:not(.single)').not(hash).each(function() {
	$(this).addClass('collapse');
	if ($(this).hasClass('videos')) { $(this).addClass('videos-collapse'); }
});

So, all <article>s (except single events and <article>s loaded by a #hash in the URL) get closed/collapsed upon page load.

// Open/close articles on click
$('article').click(function(event) {
	var $clicked = $(event.target);
	if ( ! ($clicked.is('a') && $clicked.attr('rel'))) {
		$('article:not(.collapse)').not(this).addClass('collapse');
		$(this).toggleClass('collapse');
		if ($(this).hasClass('videos')) {
			$('article:not(.videos-collapse)').not(this).addClass('videos-collapse');
			$(this).toggleClass('videos-collapse');
		}
	}
});

If it isn’t immediately obvious, clicking anywhere on the <article>s expands them – and to keep things neat for the user, only one <article> can be open at a time. Also, if the user clicks an external link within the <article>, it doesn’t make sense to close/collapse that <article>, so nothing else happens. The CSS for this is simple:

article {
	height: auto;
	overflow: visible;
	cursor: default;
}

.collapse {
	height: 48px;
	overflow: hidden;
	cursor: pointer;
}

.videos-collapse {
	height: 85px;
}

Loading/Playing #hash Content on Page Load

I wanted to have a way to link to individual audio/video tracks and have the user not only taken to the requested content when the page loads, but also to start playback right away and expand/open the track details automatically. This is actually very simple to do – you just have to trigger the click function.

The same is true for the Twitter updates and my blog headlines – for example, a link to DanielMenjivar.com/#twitter will load the twitter updates on page load:

// Load our twitter updates
var hash = window.location.hash;
if (hash == '#twitter' && $('#nav-sub-twitter').length){ load_content('twitter'); }
$('#nav-sub-twitter a').click(function () { load_content('twitter'); });

As you can see from the code above, it only works if there is a "twitter" tab on the sub-navigation (which exists on the home page and the contact page). Also, the example above doesn’t trigger the click function, (but it could have). I’m also using a custom load_content(); function which takes care of the AJAX request and changing the page title – this way I can reuse the load_content(); function to load other content as well.

Sorting a Table Using jQuery

I originally intended for the charts list on my site to be sortable using JavaScript, but when I was researching how to do it, I realized that it would involve a lot of coding on my part. It made sense to use a plugin, (why reinvent the wheel?) but I couldn’t find anything lightweight enough. In the end, I realized that if the table was sorted by PHP/MySQL and the HTML was minified, an AJAX request would actually be much smaller in file size than all the JavaScript sorting code would be. (And maybe just as fast? Or even faster?) So that’s how I’m doing it – I’m replacing the table with AJAX content and letting PHP/MySQL handle the sorting.

// Allow the Charts List Table to be sorted
$('#charts-table th a').live('click', function (event) {
	$.get('/x/charts/' + $(this).text().toLowerCase(), function(data) { $('#charts-table').replaceWith(data); });
	event.preventDefault();
});

And again I’m using the .live(); method to make sure that even the AJAX loaded table is sortable again.

Playing HTML5 Audio with jQuery

Now, finally, we get to the HTML5 Audio Part!

First, Here’s an explanation of what’s happening with the markup on the page:

  • There’s no <audio>, <embed> nor <object> element anywhere on the page – it gets created dynamically by JavaScript.
  • If the browser can play HTML5 audio, and if it thinks it can play either Vorbis or AAC audio, then I create an <audio> element. Otherwise, I create an <object> element and use QuickTime for playback. Why QuickTime? Well, because I like apple for one. Second, lots of people have iPods, which requires iTunes, which requires QuickTime, so chances are high(ish) that they’ll already have QuickTime installed. Third, QuickTime can for sure play AAC audio – it means I only have to encode audio in two formats then. And the fourth reason for using QuickTime is that it makes it a little easier to control playback – remember, if the browser chooses to use QuickTime, it means that none of the HTML5 Audio API is available to me…
  • Only one track should be playable at a time – I also need to know what track is currently playing (so I know whether to pause the track, or start playing a different one.)

Make sense so far? I hope so. Here’s the JavaScript function (used later on) that creates the <audio> element (or the <object> element):

// This is the function that returns the audio element
function createNewAudio (song) {
	var type				= ($('body').attr('id') == 'charts-samples') ? 'charts' : 'audio';
	var audiotag			= $('<audio></audio>');
	if (audiotag.get(0).canPlayType && (audiotag.get(0).canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '') || audiotag.get(0).canPlayType('audio/mp4; codecs="mp4a.40.2"').replace(/no/, ''))) {
		audiotag.remove();
		var createNewAudio	= $('<audio autoplay></audio>')
									.hide()
									.attr('id','player')
									.addClass(song)
									.append('<source src="/x/get/' + type + '/' + song + '.m4a" type="audio/mp4" />')
									.append('<source src="/x/get/' + type + '/' + song + '.oga" type="audio/ogg" />');
	} else {
		audiotag.remove();
		var createNewAudio	= $('<object></object>')
									.attr('id','player')
									.attr('width',0)
									.attr('height',0)
									.hide()
									.addClass(song)
									.attr('classid','clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B')
									.attr('codebase','https://www.apple.com/qtactivex/qtplugin.cab')
									.append('<param name="src" value="/x/get/' + type + '/' + song + '.m4a">')
									.append('<param name="autoplay" value="true">')
									.append('<param name="controller" value="false">');
	}
	return createNewAudio;
}

As you can see, I’m setting the class of the <audio> (or <object>) element to the "song" so we can track which song is playing. it’s also worth mentioning that by using .get(0) it allows me to get the first DOM element in the jQuery collection and access the HTML5 audio API…

Now, Here’s how I’m using that function and controlling playback on the page:

// Play our Audio Samples and Chart Samples (audio) on click & hash load
$('.samples .icon a, .audio .icon a').click(function (event) {
	var $icon			= $(this).parent();
	var $article		= $(this).parents('article');
	var song			= $article.attr('id');
	var $audioElement	= $('#player');

	// don’t collapse our article if it’s already open (but allow it to open if it isn’t)
	if ( ! $article.hasClass('collapse') || $icon.hasClass('pause')) { event.stopPropagation(); }

	// now check if our audio element exists, if it does, pause/play it, or create a new one…
	if ($audioElement.length) {								// if the audio element exists
		if ($audioElement.hasClass(song)) {					// if the audio element is set to the clicked song
			if ($icon.hasClass('pause')) {					// if the icon is set to pause, we should just pause the song
				if ($audioElement.is('audio')) {
					$audioElement.get(0).pause();
				} else {
					$audioElement.get(0).Stop();
				}
				event.preventDefault();
			} else {										// if the icon is not set to pause, we should resume the song (play)
				if ($audioElement.is('audio')) {
					$audioElement.get(0).play();
				} else {
					$audioElement.get(0).Play();
				}
				event.preventDefault();
			}
		} else {											// the song playing is different from what was just clicked
			$audioElement.remove();
			$audioElement = createNewAudio(song).appendTo('body');
		}
	} else {												// if the audio element doesn’t exist, always create a new one
		$audioElement = createNewAudio(song).appendTo('body');
	}

	// nothing else can be paused, and toggle this button to pause on/off
	$('article .icon').not($icon).removeClass('pause');
	$icon.toggleClass('pause');
});
if ($(hash).length && ($('#charts-samples').length || $('#media-audio').length)) { $(hash + ' .icon a').trigger('click'); }

Here’s what’s going on: (It should be easy to follow along since the code is commented pretty well, but you may have to scroll to see the comments…)

  • If the audio track details (<article>) aren’t already expanded/open, then they get expanded. (But don’t close it if it’s already open.)
  • If the audio element already exists, and is set to the song that was clicked, then toggle play/pause. Keep in mind that the way to do this differs if it’s an <audio> element or a QuickTime <object>.
  • If the song that was clicked is not the same as the track that’s currently playing, I destroy/remove the old element and create a new one.
  • If the audio element doesn’t exist, create a new one…
  • Toggle the "pause" CSS class (which replaces the play icon with a pause icon) and make sure that only one song/track has a pause icon.
  • Allow any song to be loaded using its #hash id, for example, /media/audio#dilocomoes

It looks more complex than it is, doesn’t it? Yup, that’s all there is to it!

Playing HTML5 Video with jQuery

The videos on the site use the same concept as the audio except that I obviously don’t want to hide the video… Also, the videos have their own playback controls so I don’t need to worry about controlling playback via JavaScript.

// This is the function that returns the video element
function createNewVideo (video) {
	var videotag			= $('<video></video>');
	if (videotag.get(0).canPlayType && (videotag.get(0).canPlayType('video/mp4; codecs="avc1.42401E, mp4a.40.2"').replace(/no/, '') || videotag.get(0).canPlayType('video/ogg; codecs="theora, vorbis"').replace(/no/, ''))) {
		videotag.remove();
		var createNewVideo	= $('<video autoplay controls></video>')
									.attr('width','480')
									.attr('height','360')
									.append('<source src="/x/get/videos/' + video + '.mp4" type="video/mp4" />')
									.append('<source src="/x/get/videos/' + video + '.ogv" type="video/ogg" />');
	} else {
		videotag.remove();
		var createNewVideo	= $('<object></object>')
									.attr('classid','clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B')
									.attr('codebase','https://www.apple.com/qtactivex/qtplugin.cab')
									.attr('width','480')
									.attr('height','376')
									.append('<param name="src" value="/x/get/videos/' + video + '.mp4">')
									.append('<param name="autoplay" value="true">')
									.append('<param name="showlogo" value="false">');
	}
	return createNewVideo;
}

// Play our Video Samples on click & hash load
$('.videos .icon a').click(function (event) {
	var $article		= $(this).parents('article');
	var video			= $article.attr('id');

	// don’t collapse our article if it’s already open (but allow it to open if it isn’t)
	if ( ! $article.hasClass('collapse')) { event.stopPropagation(); }

	$.fancybox(createNewVideo(video), {
		'transitionIn'		: 'elastic',
		'transitionOut'		: 'elastic',
		'centerOnScroll'	: true,
		'orig'				: $(this),
		'type'				: 'inline'
	});
});
if ($(hash).length && $('#media-videos').length) { $(hash + ' .icon a').trigger('click'); }

Like the videos, the photos and chart samples images are displayed using FancyBox as well. I’ve also used FancyBox on the iPhone JavaScripts (which are slightly different than the examples above) to render the main navigation/menu.

iPhone JavaScripts

On the iPhone version of the site, there are two notable differences – instead of being normal web links, the Twitter icon and the Facebook icon on the contact page are links to my profile in the official iPhone apps. With this change, every contact icon (Facebook, Twitter, AIM, Skype) opens in the respective app. Here’s the code for that:

// change the twiter icon’s link to use the official twitter app
$('#twitter-link').click(function(event) {
	var tweeter = $(this).attr('href').split('/');
	tweeter = tweeter[tweeter.length – 1];
	var url = 'twitter://user?screen_name=' + tweeter;
	$(this).attr('href',url);
});

// change the facebook icon’s link to use the official facebook app
$('#facebook-link').click(function(event) {
	var profile = $(this).attr('href').split('/');
	profile = profile[profile.length – 1];
	var url = 'fb://profile?id=' + profile;
	$(this).attr('href',url);
});

Lastly, incase you’ve noticed the search button on the events page, it’s actually not controlled using JavaScript – only CSS! (More on that later.) The search results are not loaded by JavaScript either, but loading #more events does indeed use AJAX.

OK, that’s it for now! I hope you’ve found something useful on here. Love it? Hate it? Have a better way? Let me know!