📜 ⬆️ ⬇️

Writing a lossless audio player in JavaScript

Good afternoon,% username%. Today I would like to share my experience in developing a prototype of an online lossless audio player.

Today, it is hardly possible to surprise someone with an audio or video player embedded directly in the web page. Existing technologies, libraries and APIs make it easy to fill the site with any media content. But there are people for whom this is not enough (including me). That is why, as a true music lover at lossless, I needed to make a browser player that supports audio format such as flac .

One article pushed me to this idea: Web player FLAC.JS (HTML5) . Having learned that there is such a wonderful framework as Aurora.js and flac format decoder for it, I could not just pass by all of this. Everything - I thought - now that my level of enthusiasm has gone too high, I have to make this player. So, let's begin…

Backend


As backend, we will have the notorious Nginx and Apache with PHP (where to go without it). The first will be responsible for the return of audio data, the second will produce a page with the player and process Ajax requests.
')
Nginx configuration with CORS support
http { sendfile on; include /etc/nginx/mime.types; default_type audio/flac; server { listen *:80; server_name as.iostd.ru; root /var/mcs/storage; location / { rewrite "^\/(([a-z0-9]{2})([a-z0-9]{2})([a-z0-9]{2})([a-z0-9]{2})[a-z0-9]{56}).flac$" /$2/$3/$4/$5/$1.flac last; if ($http_origin ~* (https?://([^/]*\.)?.?iostd\.ru(:[0-9]+)?)) { set $cors "true"; } if ($request_method = 'OPTIONS') { set $cors "${cors}options"; } if ($request_method = 'GET') { set $cors "${cors}get"; } if ($request_method = 'POST') { set $cors "${cors}post"; } if ($request_method = 'HEAD') { set $cors "${cors}head"; } if ($cors = "trueget") { add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header 'Access-Control-Allow-Credentials' 'true'; } if ($cors = "truepost") { add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header 'Access-Control-Allow-Credentials' 'true'; } if ($cors = "truehead") { add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,Range,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since'; add_header 'Access-Control-Expose-Headers' 'Accept-Ranges,Content-Encoding,Content-Length,Content-Range'; } if ($cors = "trueoptions") { add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,Range,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since'; add_header 'Access-Control-Expose-Headers' 'Accept-Ranges,Content-Encoding,Content-Length,Content-Range'; add_header 'Content-Length' 0; add_header 'Content-Type' 'text/plain charset=UTF-8'; return 204; } try_files $uri $uri/; } } } 

PHP Script "In a hurry"
 <?php error_reporting(E_ALL); ini_set('display_errors', '1'); require_once('MysqliDb.php'); // https://github.com/joshcam/PHP-MySQLi-Database-Class function cors() { if (isset($_SERVER['HTTP_ORIGIN'])) { header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}"); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Max-Age: 86400'); } if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) header("Access-Control-Allow-Methods: GET, POST, OPTIONS"); if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}"); exit(0); } } function answer($data = array(), $status = "OK", $message = "") { if(!is_array($data)) return; $rcode = array(); $rcode["status"] = $status; if($status == "ERROR" && $message != "" ) $rcode["message"] = $message; header('Content-Type: application/json'); echo json_encode(array_merge($rcode, array("result" => $data))); exit(); } function error($message) { answer(array(), "ERROR", $message); } cors(); $db = new MysqliDb ('localhost', 'root', 'hackme', 'audio'); // https://github.com/joshcam/PHP-MySQLi-Database-Class if(!isset($_REQUEST["c"])) error("Bad request"); if($_REQUEST["c"] == "tracks") { $tracks = $db->rawQuery('SELECT title, a.artist, audio FROM tracks INNER JOIN artists a ON (tracks.artistid = a.id) LIMIT 20'); answer($tracks); } error("Bad request"); 


Frontend


First of all, you need to think over all the functionality that our player will support. The most obvious ones are Play / Pause, Next, Prev, buffering and search bar, volume, track name string and time. I would also like to implement the display of album covers, playlists, search in the database of the audio library, and so on, but for now I decided to stop at the most basic. Since I didn’t often come across web development, I’m not very good at layout and design.

Here's what I got in the end:



Layout:
HTML
 <div class="player"> <div class="info"> <div class="tackinfo"><span id="artist"></span> - <span id="title"></span></div> <div class="timer"><span id="time">00:00</span></div> </div> <div class="seekbar" id="seek"> <div class="wrap"> <div id="buffer"></div> <div id="progress"></div> </div> </div> <div class="controls"> <div class="playback"> <div id="play" class="fa fa-play"></div> <div class="fb"> <div id="backward" class="fa fa-backward"></div> <div id="forward" class="fa fa-forward"></div> </div> </div> <div class="volumebar" id="volume"> <div class="wrap"> <div id="volumevalue"></div> </div> </div> </div> </div> 

Styles
 .player { border: 1px solid #D0D0D0; background-color: #F0F0F0; height: 67px; border-radius: 2px; padding: 5px;} .player .tackinfo { float: left; margin-left: 0px; } .player .tackinfo #artist { color: #0474C0; font-weight: bold; } .player .tackinfo #title { color: #787878; } .player .timer { float: right; cursor: pointer; } .player .seekbar { clear: both; cursor: pointer; padding: 5px 0;} .player .seekbar .wrap { background-color: #D0D0D0; overflow:hidden; height:5px; border-radius: 2px;} .player #buffer, .player #progress { height:100%; width:0%; } .player #buffer { background-color:#909090; } .player #progress { background-color: #0474C0; margin-top:-5px; } .controls .playback { float: left; } .controls .playback .fb { float: left; } .playback #play, .playback #backward, .playback #forward { color: #0474C0; cursor: pointer; border-radius: 2px; text-align: center; vertical-align: middle; float:left; margin-right: 2px;} .playback #play:hover, .playback #backward:hover, .playback #forward:hover { background-color: #D8D8D8; } .playback #play { font-size: 24px; height: 32px; width: 32px; line-height: 32px;} .playback #backward { font-size: 16px; height: 32px; width: 32px; line-height: 32px;} .playback #forward { font-size: 16px; height: 32px; width: 32px; line-height: 32px;} .volumebar { float: right; cursor: pointer; padding: 15px 0; width:80px;} .volumebar .wrap { background-color:#D8D8D8; overflow:hidden; height:5px; border-radius: 2px; } .volumebar #volumevalue { height:100%; width:0%; background-color: #0474C0; } 

Fine. We have the necessary minimum. Now you need to revive all this. Therefore, go to JavaScript.

Create a Playlist class which, as you already understood, will be responsible for the playlist:

 Playlist = function() { this.list = []; this.current = 0; this.repeatmode = 0; }; 

Where list is the list itself, current is the current track number, repeatmode is the repeat mode (0 means no repeat, 1 repeats the entire list, 2 repeats one track).

Further we implement all the necessary methods.

Add track:
 Playlist.prototype.add = function(track) { this.list.push(track); }; 

Getting the current track:
 Playlist.prototype.getCurrent = function() { return this.list[this.current]; }; 

Methods forward, backward:
 Playlist.prototype.next = function() { if(this.repeatmode == 2) { return this.current; } if(this.current >= this.list.length - 1) { if(this.repeatmode == 0) { return -1; } else if(this.repeatmode == 1) { return (this.current = 0); } } return ++this.current; }; Playlist.prototype.prev = function() { if(this.current == 0) return this.current; return --this.current; }; 

And finally, the method of mixing the list:
 Playlist.prototype.shuffle = function(){ for(var j, x, i = this.list.length; i; j = Math.floor(Math.random() * i), x = this.list[--i], this.list[i] = this.list[j], this.list[j] = x); }; 

We have a playlist, go to the player itself. Create a class Musica:

 Musica = function(params) { this.ui = { artist: params.artist, title: params.title, seekbar: params.seekbar, bufferbar: params.bufferbar, progressbar: params.progressbar, timer: params.timer, playbtn: params.playbtn, backwardbtn: params.backwardbtn, forwardbtn: params.forwardbtn, volumebar: params.volumebar, volume: params.volume }; this.pstate = 0; this.seekstate = 0; this.timetype = 0; this.aurora; this.volume = 100; this.playlist = new Playlist(); this.ui.timer.click((function (_this) { return function(e) { _this.timetype = _this.timetype == 0 ? 1 : 0; _this.setTimer(_this.aurora.currentTime); }; })(this)); this.ui.playbtn.click((function (_this) { return function(e) { if(_this.pstate == 0) _this.play(); else _this.pause(); }; })(this)); this.ui.backwardbtn.click((function (_this) { return function(e) { _this.prev(); }; })(this)); this.ui.forwardbtn.click((function (_this) { return function(e) { _this.next(); }; })(this)); }; 

In params we will place all interface elements using jQuery selectors, using the pstate variable , we will determine the status of the player (play / not play), seekstate will be useful for us when we implement the search bar, and timetype determines the type of timer (how long or how much is left) . Also in this constructor, we immediately hung up event handlers on all available buttons.

The Aurora.js framework contains the Player class, which implements the minimum we need. It has such methods as play (), pause (), stop (), seek (), and also an event handler is implemented. This greatly simplifies our task.

Let's try to implement the player initialization method:
 Musica.prototype.open = function() { if(this.aurora) this.aurora.stop(); this.aurora = AV.Player.fromURL('http://as.iostd.ru/' + this.playlist.getCurrent().audio + '.flac'); this.aurora.volume = this.volume; this.ui.volume.css('width', ((this.volume * 100 ) / this.ui.volumebar.width())+'%'); this.pstate = 0; this.ui.playbtn.removeClass("fa-play fa-pause").addClass("fa-play"); this.ui.artist.html(this.playlist.getCurrent().artist); this.ui.title.html(this.playlist.getCurrent().title); this.ui.bufferbar.css('width', '0%'); this.ui.progressbar.css('width', '0%'); this.aurora.on('buffer', (function (_this) { return function(percent) { _this.ui.bufferbar.css('width', percent+'%'); }; })(this)); this.aurora.on('progress', (function (_this) { return function(time) { if(_this.seekstate == 0) _this.ui.progressbar.css('width', ((time * 100 ) / _this.aurora.duration)+'%'); _this._setTimer(time); }; })(this)); this.aurora.on('end', (function (_this) { return function() { _this.next(); }; })(this)); this.aurora.preload(); }; 

Here we load the current track from the playlist into the framework, reset the GUI to default values ​​and connect event handlers. We will call this method every time we need to play a new track.

Now we need to do, in my opinion, the most difficult thing: the search bar and volume control. These two elements of the interface are very similar to each other (at least in our case).

  this.ui.seekbar.off(); this.ui.seekbar.mousedown((function (_this) { return function(e) { var offsetx = e.offsetX; var origin = $(this); _this.seekstate = 1; _this.ui.progressbar.css('width', ((offsetx * 100 ) / $(this).width())+'%'); $(document).mousemove(function(e) { offsetx = e.pageX - origin.offset().left; offsetx = offsetx < 0 ? 0 : (offsetx > origin.width() ? origin.width() : offsetx); _this.ui.progressbar.css('width', ((offsetx * 100 ) / origin.width())+'%'); }); $(document).mouseup(function(e) { $(document).off("mousemove"); $(document).off("mouseup"); _this.aurora.seek(Math.floor((offsetx * _this.aurora.duration) / origin.width())); _this.seekstate = 0; }); }; })(this)); 


First, we connect the mousedown event handler. After that, we change the progressbar value and connect two more handlers. In the first ( mousemove ), we also change the progressbar value. In the second ( mouseup ), we disable these two handlers and call the seek () framework method. In order that during the search with the mouse, the progressbar does not twitch from the progress event that we process above, we need to seekstate .

In the same way we do the volume control:
  this.ui.volumebar.off(); this.ui.volumebar.mousedown((function (_this) { return function(e) { var offsetx = e.offsetX; var origin = $(this); _this.ui.volume.css('width', ((offsetx * 100 ) / origin.width())+'%'); _this.volume = Math.floor((offsetx * 100) / origin.width()); _this.aurora.volume = _this.volume; $(document).mousemove(function(e) { offsetx = e.pageX - origin.offset().left; offsetx = offsetx < 0 ? 0 : (offsetx > origin.width() ? origin.width() : offsetx); _this.ui.volume.css('width', ((offsetx * 100 ) / origin.width())+'%'); _this.volume = Math.floor((offsetx * 100) / origin.width()); _this.aurora.volume = _this.volume }); $(document).mouseup(function(e) { $(document).off("mousemove"); $(document).off("mouseup"); _this.volume = Math.floor((offsetx * 100) / origin.width()); _this.aurora.volume = _this.volume; }); }; })(this)); 


The player is almost ready. We only need to add the methods play (), netxt (), prev (). There is nothing difficult in their implementation:

 Musica.prototype.play = function() { this.aurora.play(); this.pstate = 1; this.ui.playbtn.removeClass("fa-play fa-pause").addClass("fa-pause"); }; Musica.prototype.pause = function() { this.aurora.pause(); this.pstate = 0; this.ui.playbtn.removeClass("fa-play fa-pause").addClass("fa-play"); }; Musica.prototype.next = function() { if(this.playlist.next() !== -1) { var pss = this.pstate; this.pause(); this.open(); if(pss == 1) { this.play(); } } }; Musica.prototype.prev = function() { if(this.playlist.prev() !== -1) { var pss = this.pstate; this.pause(); this.open(); if(pss == 1) { this.play(); } } }; 


Is done. You can connect the player.

We connect
 $(function() { var params = { artist: $('#artist'), title: $('#title'), seekbar: $('#seek'), bufferbar: $("#buffer"), progressbar: $("#progress"), timer: $("#time"), playbtn: $("#play"), backwardbtn: $("#backward"), forwardbtn: $("#forward"), volumebar: $("#volume"), volume: $("#volumevalue") }; var mplayer = new Musica(params); $.get( "http://iostd.ru/audioapi.php?c=tracks", function( data ) { for (var i = 0; i < data.result.length; i++) { mplayer.playlist.add(data.result[i]); } mplayer.open(); mplayer.play(); }); }); 


PS As you can see, it is not so difficult to make a player like VK.

Demo: http://audio.iostd.ru/
Sources: https://github.com/Show-vars/HTML5LosslessPlayer

Source: https://habr.com/ru/post/235741/


All Articles