📜 ⬆️ ⬇️

Play head

Experience of declarative programming in JavaScript on the example of an audio player

Author - Rostislav Chebykin.
Layout and placement on Habr - den_lesnov .

I feel something so wrong
By doing the right thing ...
Ryan Tedder (OneRepublic). Counting stars

')
Denis Lesnov and I developed an audio player for my site . The site contains audio recordings of songs, and I have long dreamed of making them play directly from web pages.

The player looks like this:



How it works - you can see on the demo page .

The first question we are asked is: why did we townline our own player from scratch, and not use any of the hundreds of ready-made solutions? The answer is simple: because we were interested in this task.

In this project there were no customers, bosses, financial motivation and certain deadlines. We met about once a week at one of us at home and programmed for our own pleasure. The first valid version of the player was ready in two evenings and uploaded to the site, and then for about a year we brought the code into a divine form.

Here we will talk about the main technical solutions that we used.



Technical Kosher


- Wow, we just started yesterday, and we already have a legacy code ...

The player works on modern client technologies (HTML5 Audio object), without Flash and other antiques. However, users of old browsers do not suffer: for them, the player behaves as a direct link to the MP3 file. This is achieved without checking for specific models and versions of browsers, but only through feature detection.

We did not use jQuery and other libraries, all code is written in pure JavaScript. This is also because it is so interesting.

In JavaScript, we followed the “minimum imperative, maximum declarative” approach, and also tried to avoid duplication of code, “omnipotent” functions, multi-storey conditions and cycles, and other code smells.

The player is "rubber" horizontally: it automatically stretches across the width of the container, even if this width changes during sound.

HTML: less is more


can not moses people
read the tables
they probably encoded
koi

The HTML code associated with the player, in its initial state, is the usual direct hyperlink to the audio file:

<a href="/path/file.mp3" target="_blank" id="player" class="ready"> </a> 


This is convenient if you do not want to start the music, but save the file to disk, open it with another application, copy the link and so on.

If you click on the link, JavaScript will check if the environment is ready to play the file. If not ready - the link works in the usual way. The same thing happens if javascript doesn't start at all.

Finally, if playback is available, the content of the link becomes:

 <a target="_blank" id="player" class=""> <track></track> <playhead draggable="true"></playhead> </a> 


In the active state of the player, the HTML code consists of only three elements: exactly one for each component of the interface with which you can interact:


Design without haste


- Here we need to mimic the code ...
- It’s good that you don’t get it!

The design was based on a model representing the player as a set of core functions that were independent of the implementation. In order to identify the most universal model, we wondered what would have happened if the system were not a web interface, but consisted of concrete blocks or in general represented a live minstrel with a lute.

At this stage, we fixed, for example, that starting and stopping music are the fundamental capabilities of the player, and “changing the value of left for the head” is the implementation details. This helped not to mix abstractions of different levels in the code.

Refining the model, we drew a lot of charts and tables and accidentally came up with the original version of ER-modeling, which I will tell about another time.

We are sensitive to the names of variables, CSS classes and other entities. We could spend an hour or two on the selection of a successful name, for which then will not be ashamed. For example, at first we called the slider either slider or indicator until we found out that in English there is an old playhead term that means exactly what you need. We were so happy that we began to use the comic translation "Play-head" as the unofficial name of the entire project.

CSS: geometry without images


- It looks native, and look disgusting ...

We did not use pictures; All player geometry is drawn using CSS. For example, here's an icon :

 #player::before { content: ''; float: left; margin-right: 6px; width: 0; height: 1px; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-left: 10px solid #999; } 


Actually, the icon / implemented as pseudo element :: before element a. This simplified JavaScript: to start and stop the music, click on the player itself , rather than on a special element, is processed.

Similarly, to depict the filling of the buffer, we did not introduce a separate element, but used the linear-gradient function as background-image for the track. In the initial state, the buffer is empty, and the track is filled with white:

 track { background-image: linear-gradient(to right, #ddd 0%, #fff 0%); } 


The percentage value after #ddd is incremented in accordance with the buffer fullness, and part of the track becomes gray. When the buffer is full, the entire track is grayed out.

System states


- This is not a crutch, but an adapter!

We decided that at each moment of time the system can be in one of three states:


Distinguishing between states is important for the system to react differently to the same events. For example, clicking the player in the SET state causes it to expand its entire infrastructure, and in the GO state, it causes it to start or stop the music.

States helped build the code more declaratively. In the first version of the program it was impossible to breathe from addEventListener / removeEventListener strung together, but to the final version this one went completely to the side.

The stateCtrl special object helps to monitor the states. For example, stateCtrl.is ('SET') returns true if and only if the system is in the SET state.

All behavior in one place


- What do you think about unit tests?
- You know, Kozma Prutkov on this occasion was an aphorism ... only I forgot what. That's all I think about unit tests.

Perhaps from this place the most enthusiastic begins. All system behavior is described by one structure, which I will show in its entirety:

 var behavior = new Behavior({ window: { DOMContentLoaded: { READY: 'player.set' } }, html: { dragover: 'playhead.pull', drop: ['audio.land', 'stateCtrl.toggle'] }, audio: { timeupdate: { NO_DRAG: ['playhead.move', 'track.augment'] }, progress: 'track.augment', ended: ['audio.pause', 'player.toggle'] // 1 }, player: { click: { SET: ['audio.start', 'player.go', 'stateCtrl.nextActivityState'], GO: 'audio.toggle' // 2 } }, playhead: { dragstart: 'playhead.drag' }, track: { click: 'audio.rewind' } }); 


Here we describe what events we expect at what facility and in what state and what to do in response to each event. For example, line 1 means that when the ended event occurs on the audio object (in any state), the functions audio.pause and player.toggle are called. Line 2 means that the click event on the player object (this is the player itself is element a in HTML), if the system is in the GO state, the audio.toggle function is called.

In fact, the rest of the code is dedicated to “breathing life” into this purely declarative description and make it work.

The same and preventDefault


“Well, two handlers started dragging the playhead and tore it up ...”

In response to some events, it is necessary not only to perform certain actions, but also to prevent the default reaction (for example, following a link). We decided not to mix it with the behavior described earlier, because this does not apply to the player’s “business logic”, but to the features of event handling in the browser.

A separate structure describes where to hang the preventDefault:

 var modifiers = { preventDefault: ['html', 'track', 'html.dragenter', 'player.click.SET'] }; 


This means that preventDefault will be added to the handlers:


The whole implementation is also in one place.


- Say a rhyme to the word eval!
- setInterval ... Oh, no, this is a bad rhyme ... That is, a rhyme is good, the idea is bad.

The implementation of all functions is also described in a separate structure, built according to this scheme:

 var impl = { Number: { toPercentString: function() { /* … */ } }, Array: { modify: function(handler) { /* … */ } }, String: { createListeners: function(behavior) { /* … */ }, getObject: function() { /* … */ }, modify: function(handler) { /* … */ }, }, HTMLElement: { move: function() { /* … */ }, // …  . . … }, // …  . . … }; 


The keys of this structure are the names of "classes" (more precisely, prototypes), and the values ​​are a list of functions that will be called from instances of these "classes". For example, the move function is called from a playhead object whose prototype is an HTMLElement.

In the current system, functions are attached directly to the corresponding prototypes, which spoils the code kosher. Further we will adjust more accurate inheritance.

A separate feature: if the name of a function in impl starts with get, then it is attached as a full-fledged getter. For example, the function getObject turns into a property that can be called as a string.object.

How things are going


- The plan is this: first we write the unclean function, then we make it clean. Laundering features!

When the script is launched, special black magic is triggered, which digests all this declarativeness and eventually makes the music sound:


From this, DOMContentLoaded immediately fires on the window, and everything takes off. The rest of the handlers are hung inside individual functions at the right moments. For example, handlers for the html, player and audio objects are created inside the set function, which starts after the page loads, and the handlers for track and playhead are inside the go function, which starts after the player is activated:

 ['playhead', 'track'].createListeners(behavior); 


As a result, the word "addEventListener" is mentioned exactly once in all the code.

More about black magic


- We need to go thiser ...

I mentioned black magic, because attempts to program in JavaScript in a declarative style inevitably lead to mystical spells in the code. For example, the createListeners method recursively circumvents the insides of behavior using the polymorphic walk method, which is defined like this:

 for(var typeName in walkers) { // Object, Array, String window[typeName].addProperties({ walk: functools.partialLeft( function(name, params) { walkers[name].call(this, params); }, typeName ) }); } 


Here we pulled a piece of Python by the ears: in order for this code to work, we conjured partialLeft and put it in the “module” of functools.

In turn, partialLeft uses the auxiliary function array, which turns everything into an array. What is not black magic?

Moral of this fable


- Do you again refactor non-existent code?

As you have noticed, there is almost nothing in this narration about working with the Audio object and about other specific features of media players. This is because in this area we just did not produce anything stunning. Our player makes a sound about the same as hundreds of other analogs.

But the declarative architecture, in the center of which lies the object of behavior with all the behavior of the system, seems to us promising for use not only in this player, but also in other projects. Perhaps, in time, a library will grow out of this, which will disgrace and supplant all others.

In the meantime, Denis and I continue to polish the existing code and are going to add playlists to it so that you can listen to entire albums on the site.

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


All Articles