User:Laurier Rochon/prototyping/jsplaylist

From XPUB & Lens-Based wiki

HTML5 Audio player /w integrated playlist rendering

Player html5.jpg

An example of the player loaded with Kid Koala's awesome live show /w P Love in Montreal.


And I've also uploaded the newest version on BitBucket


Live on the server here : http://pzwart3.wdka.hro.nl/~lrochon/playlist_1/


  • I made a simple system that generates a playlist out of a JSON file.
  • It's the first iteration, so it's a little shabby
  • Dependencies : ffmpeg, php, bash, a recent browser
  • Note : when running locally Chrome, beware of the allow-file-access bug. Run with "chromium-browser --allow-file-access-from-files" otherwise JSON loading via jQuery will break.


How it works

  • You put MP3 files in a folder (it is assumed most people have mp3 files to start with)
  • Run mk.php either locally or on the server, three things happen : A)It first invokes rename.sh which takes care of renaming the tracks to make sure there's no funny characters, spaces, etc. (within the same folder) B)for every track, an OGG version is created, using the same same C)a JSON file is created, which is then used for the playlist
  • On loading the page :
- Basic HTML code and controls are loaded, I replaced the default player with my own
- JS checks if it can load MP3s or OGG files in the browser. This is then used throughout. OGG is given precedence if both can be played.
- An <audio> tag is being created with the first element of the playlist in the source
- For every track in the playlist, create a element which is clickable and bound to the play function
- Most of whatever code is leftover is just binding and checking for states. For example the next/prev buttons, play/pause toggle, updating time, etc.
- Small interesting thing : it seems like waiting for "data to finish loading" and attaching a callback sometimes flakes out in certain browsers, making it difficult to judge when to start playing the next track. Apparently using audio.load() before audio.play() helps prevent this. I've tried it and had no difficulties streaming the files off the server at all. Would need to test behavior on a slow connection.


Improvements

  • Make this playlist an object so it is easy to have many on a single page. It's already somewhat crafted in that direction with a bunch of functions waiting to be methods and a block at the bottom which would easily be turned into the constructor. Then one could easily send the playlist URL to the constructor as a parameter and we're set. JS's prototype OOP system...hmm.
  • Change the links to reference the MP3 files from a level higher (or more) if necessary. Just to avoid mass downloading of the files if they are copyrighted - especially if on a page with many of these playlists.
  • More error checking. There is not much feedback given for error right now.


Soft

index.html

<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="jquery1.7-min.js" type="text/javascript"></script>
<link href='http://fonts.googleapis.com/css?family=PT+Sans+Narrow:400,700' rel='stylesheet' type='text/css'>

<style type="text/css">
#bts{
	font-family: 'PT Sans Narrow', sans-serif;
	width:500px;
	overflow:hidden;
}
.top{
	background:#eee;
	font-weight:700;
}
.playing{
	min-width:200px;
}
.tracktitle{
	background:#efefef;
}
a{
	text-decoration:none;
	color:#940000;
}
#play,#prev,#next{
	text-transform:uppercase;
}
.tracktitle{
	cursor:pointer;
}
.tracktitle:hover{
	background:#ddd;
}
</style>

</head>

<body>


<div id="bts">
<table cellpadding="6" cellspacing="6" id="player" width="500">
<tr>
<td class="top" width="50"><a id="play" href="javascript:void(0)">Play</a></td>
<!--<td class="top playing"><span id="playing"></span></td>-->
<td class="top" width="100"><span id="tleft">0:00</span></td>
<td class="top"><a id="prev" href="javascript:void(0);">Prev</a></td>
<td class="top"><a href="javascript:void(0);" id="next">Next</a></td>
</tr>
</table>
</div>





</body>
</html>


javascript

(externally linked or just within the HEAD)

<script type="text/javascript">

function addBobby(url,element){
	$.getJSON(url, function(data){
		if(data){
			Bobby(data,element)
		}
	}).error(function(){alert("Cannot find your JSON file '"+url+"'. Check it again?")})
}
function Bobby(playlist,element){
	
	var playlist
	var uuid = idgen()
	var format
	var counter = 0
	var lastcounter

	init()

	function init(){
		tempaudio  = document.createElement("audio")
		canPlayMP3 = (typeof tempaudio.canPlayType === "function" && tempaudio.canPlayType("audio/mpeg") !== "");
		canPlayOGG = (typeof tempaudio.canPlayType === "function" && tempaudio.canPlayType("audio/ogg") !== "");
		
		//deterine what's playable / use OGG if both are available
		if(canPlayMP3){ format = "mp3" }
		if(canPlayOGG){ format = "ogg" }

		skeleton = '<div id="'+uuid+'">\
						<audio src="'+playlist[counter]+'.'+format+'"></audio>\
						<table cellpadding="6" cellspacing="6" class="player">\
						<tr>\
						<td class="top playtr"><a class="play" href="javascript:void(0)">Play</a></td>\
						<td class="top timeleft"><span class="tleft">0:00</span></td>\
						<td class="top nexttr"><a class="prev" href="javascript:void(0)">Prev</a></td>\
						<td class="top nexttr"><a class="next" href="javascript:void(0)">Next</a></td>\
						<td class="top"> </td>\
						</tr>\
						</table>\
						</div>'
		if(!element){
			$("body").append(skeleton)
		}else{
			e = $(element)
			if(e.length==0){alert("Couldn't find element "+element+", check your html?")}else{
				e.append(skeleton)
			}
		}
		

		$.each(playlist, function(index, value) {
			basename = value.split("/")
			$("#"+uuid+" table.player").append('<tr class="track"><td class="track'+index+' tracktitle" data-trackno="'+index+'" colspan="5">'+(index+1)+'. '+spaceout(basename[basename.length-1])+'</td></tr>')
		});
		
		audio = $('#'+uuid+' audio').get(0);

		bind(audio)
    	updateplaying()
	}	

	function bind(audio){
		$(audio).bind('timeupdate', function() {
		  rem = parseInt(audio.duration - audio.currentTime, 10),
		  pos = (audio.currentTime / audio.duration) * 100,
		  mins = Math.floor(rem/60,10),
		  secs = rem - mins*60;
		  if(secs<10){
		  	secs = "0"+secs
		  }
		  $("#"+uuid+" .tleft").html(mins+":"+secs);
		});

		//add classes playing/not playing
		$(audio).bind('play',function() {
		  $("#"+uuid+" .play").addClass('playing');   
		}).bind('pause ended', function() {
		  $("#"+uuid+" .play").removeClass('playing');
		}).bind('ended', function() {
		  next(audio, playlist)
		});

		//play individual tracks
		$("#"+uuid+" table td.tracktitle").bind('click',function() {
			trackno = $(this).data().trackno
			flipstate("Pause")
			counter = trackno
			index = trackno
			pushplay(audio, playlist[index])
		});

		//play/pause
		$("#"+uuid+" .play").bind('click',function() {  
  			if (audio.paused) {
  				audio.play();
  				flipstate();
	  		}else{
	  			audio.pause(); 
	  			flipstate();
	  		}
		});

		$("#"+uuid+" .next").bind('click',function() {
			next(audio, playlist)
			flipstate("Pause")
		});

		$("#"+uuid+" .prev").bind('click',function() {
			prev(audio, playlist)
			flipstate("Pause")
		});
	}


	//taken from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
	//id mainly for CSS reference
	function idgen() {
	    var S4 = function() {
	       return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
	    };
	    return (S4()+S4()+S4());
	}

	function spaceout(string){
		string = string.replace(/_/g," ")
		return string
	}

	function flipstate(state){
		if(state){
			$("#"+uuid+" .play").html(state)
		}else{
			if($("#"+uuid+" .play").html()=="Pause"){
				$("#"+uuid+" .play").html("Play")
			}else{
				$("#"+uuid+" .play").html("Pause")
			}
		}
	}

	function updateplaying(){
		if(typeof lastcounter != 'undefined'){
			current = $("#"+uuid+" .track"+lastcounter);
			current.css("background","#EFEFEF");
			current.css("color","#000");
		}
		current = $("body #"+uuid+" .track"+counter);
		current.css("background","#999");
		current.css("color","#fff");
		lastcounter = counter
	}

	function pushplay(audio, track){
		audio.setAttribute("src", track+"."+format);
		msg = $("#"+uuid+" .tleft")
		msg.html("loading...")
		res = audio.load()
		audio.play()
		updateplaying()
	}

	function next(audio){
		if(counter<playlist.length-1){
			counter++
			pushplay(audio, playlist[counter])
		}
	}

	function prev(audio){
		if(counter!=0){
			counter--
			pushplay(audio, playlist[counter])
		}
	}

}

</script>


mk.php

<?php

  function d($directory) 
  {
    $results = array();
    $handler = opendir($directory);

    while ($file = readdir($handler)) {
      if ($file != "." && $file != ".." && strstr($file,".mp3")) {
        $results[] = $file;
      }
    }
    closedir($handler);
    return $results;

  }

  //clean up names first
  shell_exec("./rename.sh");

  $res = d(".");
  $a = array();

  sort($res);

  foreach($res as $file){
    $newname = substr_replace($file , 'ogg', strrpos($file , '.') +1);
    $noext = substr_replace($file , '', strrpos($file , '.'));
    echo "making file {$newname}\n";
    shell_exec("ffmpeg -i ".$file." -y -acodec vorbis -aq 60 ".$newname."");
  	array_push($a,$noext);
  }


  $tracks = json_encode($a);
  $f = fopen("tracks.json","w");
  fwrite($f,$tracks);
  fclose($f);

?>


rename.sh

#!/bin/bash

ls | while read -r FILE
do
    mv -v "$FILE" `echo $FILE | tr ' ' '_' | tr -d '[{}(),\!]' | tr -d "\'" | tr '[A-Z]' '[a-z]' | sed 's/_-_/_/g'`
done


tracks.json

["04.kid_koala_+_p_love_interlude_1","05.kid_koala_+_p_love_nufonia_must_fall_opening_credits","06.kid_koala_+_p_love_nufonia_must_fall_track_1","07.kid_koala_+_p_love_interlude_2","08.kid_koala_+_p_love_nufonia_must_fall_trailer_music","09.kid_koala_+_p_love_interlude_3"]