Introduce yapi.js, an adaptive streaming web player.
This slides has 3 main topics:
1. How yapi.js implement adaptive streaming with HTML5 media element and MSE (Media Source Extension).
2. How to create ui for playback with native javascript.
3. Compare jasmine and mocha, and some tips about unit test.
6. VIDEO ELEMENT
var videoNode = document.createElement(‘video’);
videoNode.src = VIDEO_URL;
With html5 video element, you can play single video source easily
more info here: https://goo.gl/Fyi3Z3
7. MSE
“ MSE (Media Source Extension) extends HTMLMediaElement to allow
JavaScript to generate media streams for playback.
Allowing JavaScript to generate streams facilitates a variety of use cases like
adaptive streaming and time shifting live streams.“
8. var video = document.createElement(‘video’);
video.src = VIDEO_URL
MEDIASOURCE IS A ‘SOURCE’
set ‘src’ attribute of video element to an url pointed to media
source
new window.MediaSource();window.URL.createObjectURL(ms);
9. SOURCE BUFFER
sourceBufferVideo = ms.addSourceBuffer(VIDEO_CODEC);
sourceBufferAudio = ms.addSourceBuffer(AUDIO_CODEC);
// get stream buffer via network
sourceBufferVideo.appendBuffer(buffer);
// sourcebuffer provides buffer info after append complete
10. BUFFER INFO
var buffered = sourceBuffer.buffered;
buffered.length; // how many discontinuous buffered time range
buffered.start(0); // first buffer start time
buffered.end(0); // first buffer end time
get buffer information from buffered attribute
11. ADAPTIVE STREAMING
// assume segment of time 0s~5s is needed
// yapi would decide which bitrate to download
loadSegment(segment_1_240p_url);
// segment file doesn’t have meta data, we have to append it first
sourceBuffer.appendBuffer(init_240p_buffer);
// then loaded segment
sourceBuffer.appendBuffer(segment_1_240p_buffer);
// if yapi decide to load 480p for 5~10s (adaptive algorithm)
// then yapi repeat same process, but with 480p
// then playback would be 240p during 0~5s and 480p for 5~10s
12. GRAPH
yapi append stream buffer to
sourcebuffer ‘adaptively’
Media
Source
Media
Element
media element plays
while available
13. MSE EXTENDS MEDIA ELEMENT
• MSE focus on providing stream buffer to media
element
• playback behavior still hold by media element
15. USEFUL EVENTS FORYAPI.JS
loadstart: Indicate loading media begins, this fires when setting src attribute
loadedmetadata: while element has basic info of playback, e.g duration
timeupdate: while current time of playback is ticking
seeking/seeked: while conducting seek
ended: playback ends
play/playing: playback resume from other status to playing
17. ADDITIONAL EVENTS
loadedmanifest: after manifest is loaded/parsed
bitratechanged: when bitrate is changed
enableabr: when adaptive activation changed
buffering: when playback pending
cuechanged: webvtt subtitle cue changed (in and out)
19. SEEK BEHAVIOR 1
• mediaElement.currentTime = TIME_SEEK_TO
will conduct seek
• use yapi.seek(TIME_SEEK_TO) instead
20. SEEK BEHAVIOR 2
• seek is totally done by mediaElement, so yapi.js listen to
seeking and seeked event to know seek starts and ends
• seeking event dispatched by yapi would have seekFrom and
seekTo as parameters
21. PENDING DETECTION
• No useful event to indicate pending situation
waiting stalled emptied
• pending detection is done on every timeupdate event, yapi.js
would check media source buffered length
22. PLAYBACK END
• mediaElement dispatch ended event natively when playback
goes to end
• but not while attaching media source to it
• mediaSource.endOfStream() must be invoked beforehand
24. SUMMARY
• media source provide stream buffer for mediaElement
mediaElement.src = createObjectURL(ms)
• listen to events dispatched by yapi, instead of mediaElement
• use api exposed by yapi (call api of mediaElement not
recommended) e.g seek
25. M3U8 ON SAFARI
• attach m3u8 to mediaElement on safari by simply
mediaElement.src = URL_TO_M3U8
30. SLIDER CLASS
• slider constructor takes a node and orientation as
mandatory parameters
• it focus on
1. locate pointer with given param (event or percentage)
2. return location with given param
3. notify listeners events occurred
31. TAKE A LOOK AT PROGRESS BAR
• played bar
• buffered bar
• seek time indicator
• preview
33. BEHAVIOR FLOW
scenario 1: progress changed by app. e.g yapi.seek(TIME) invoked
MediaCtrlPanel ProgressBar slideryapi
scenario 2: progress changed by user interaction, e.g click on slider
MediaCtrlPanel ProgressBar slideryapi
34. SCENARIO 1: FROM APP
MediaCtrlPanel.onSeeking(event)yapi.seek(time)
MediaCtrlPanel listens to ‘seeking’ event emitted from yapi,
then calculates percentage of current progress
(currentTime / totalDuration)
35. SCENARIO 1: FROM APP
MediaCtrlPanel. ProgressBar.locatePointer(percentage)yapi.seek(time)
36. SCENARIO 1: FROM APP
MediaCtrlPanel. ProgressBar.locatePointer(percentage)
slider.locatePointer(percentage)
yapi.seek(time)
progressBar update played bar
37. SCENARIO 1: FROM APP
MediaCtrlPanel. ProgressBar.locatePointer(percentage)
slider.locatePointer(percentage)
yapi.seek(time)
slider.locatePointer would invoke method locatePointerInternal,
with1. it’s argument 2. notToDispatchEvent as true (I know it’s tricky)
38. SCENARIO 2: FROM SLIDER
assume user conduct seek by click on slider, b/c
slider.addEventListener(‘click', locatePointerInternal);
so it will dispatch POINTER_LOCATED event
slider
(Remember notToDispatchEvent param?)
39. SCENARIO 2: FROM SLIDER
progressBar listens to ‘POINTER_LOCATED’ event and
update played bar
ProgressBar slider
40. SCENARIO 2: FROM SLIDER
MediaCtrlPanel also listens to ‘POINTER_LOCATED’ event
and it will invoke yapi.seek(calculatedTime)
(percentage of time is within POINTER_LOCATED event obj)
MediaCtrlPanel
ProgressBar
slideryapi
41. A LESSON
“ take care of flow direction, or it might be an infinite loop “
MediaCtrlPanel ProgressBar slideryapi
yapi MediaCtrlPanel ProgressBar slider
43. LISTENTO EVENT
consider on these 2 phrases
“ progressBar listens to ‘POINTER_LOCATED’ event “
“ MediaCtrlPanel also listens to ‘POINTER_LOCATED’ event “
44. BAD IMPLEMENTATION
but it works!
progressBar.addEventListener();mediaCtrlPanel.addEventListener();
slider.addEventListener(POINTER_LOCATED, callback);
45. ADD LISTENER
it would call method with same name of eventBus
slider eventBus.addEventListener(POINTER_LOCATED, callback)
while invoking this, eventBus will manipulate an object holding
map of event name and callback.
in the case, POINTER_LOCATED would be key,
and an array as value with callback pushed into it.
46. DISPATCH EVENT
POINTER_LOCATED is an event from slider
it will find array of key: POINTER_LOCATED,
and invoke every element (callback) of it.
slider eventBus.dispatchEvent(POINTER_LOCATED)
47. WHAT’STHE BAD PART?
• mediaCtrlPanl needs a reference of slider
MediaCtrlPanel ProgressBar sliderconflicts with
mediaCtrlPanel.addEventListener();
slider.addEventListener();
• for every dispatcher, an eventBus instance is needed
slider eventBus.addEventListener() eventBus.dispatchEvent()
assume progressBar would dispatch a ‘PROGRESS_BAR_HIDE’ event,
then every module which listens this event needs progressBar reference,
and progressBar needs a private eventBus for mapping
progressBar? another eventBus
48. TAKE A LOOK AT DOM EVENT
while click on <td>
<table> would also get it
“by default”
http://www.w3.org/TR/DOM-Level-3-Events/
HOW COME?
<td>.addEventListener(‘click’, callback)
<table>.addEventListener(‘click’, callback)
49. TAKE A LOOK AT DOM EVENT
http://www.w3.org/TR/DOM-Level-3-Events/
a singleton eventBus holds
by every node
while add listener
it knows which node listens
while dispatching
it invoke callback with node as context
53. INITIALIZATION DETAIL
• in client app, invoke new MediaCtrlPanel()
• in panel.setup(), loads template html
• after html loaded, apply component functions
also initialize progress-bar and volume-ui
54. IF A MODULE HANDLINGTHIS
• that module called ‘yapiDOM’ with a method ‘render’
• takes 2 parameters:1. constructor of a component,
2. selected node this component would mount on
• while `render()` invoked, yapiDOM will instantiate given
constructor and run setup method of component
(load template and apply component functions )
57. IFYAPIDOM ALSO HANDLESTHIS
• yapiDOM has another method ‘createElement’
• takes 2 parameters:1. constructor of a component,
2. object with map of key/variable
• while `createElement()` invoked, yapiDOM will instantiate
given constructor and run setup method of component
58. SETUP COMPONENT
1. load html (js sync / html async)
2. apply component functions
3. hold reference of given map object
4. return created component node
61. SUMMARY
Component
1. have a conventional ‘setup’
method with yapiDOM
2. setting these while setup:
• template
• behavior
• reference
yapiDOM
1. createElement method:
run instance method ‘setup’ of
given component class
2. render method:
append node to target node
attach singleton eventBus here
66. RUN ON COMMAND LINE
• can be run on phantomJS, a headless browser
• easier integrate with CI
67. TASK RUNNER
• grunt-mocha takes html file as
source
• for grunt-jasmine, set up
source, vendor, spec
javascript files in task config
68. WHICH I PREFER
• flexibility comes with complexity
• mocha can do everything, so it’s important that you
make choice and being consistent
• jasmine is capable enough for client-side unit test
69. TDD / BDD
• mocha.setup() can decide which strategy to use (link)
• it’s just the difference between syntax
70. TIPS OF WRITINGTEST
• it doesn’t matter which strategy to adopt, just get your hands
dirty
• get code coverage 100%
• add relevant test case while adding feature (b/c there must be a
reason you do this)
• also while fixing bug (prevent from occurs again)
72. CONSIDERTHESE FEATURES
• watch on modification
• preprocess
• run on different browsers simultaneously
• code coverage report
73. KARMA IS…
• a test runner
• would spawn a server by default on port 9876
which serves test cases
74. CONFIG KARMA
• list needed plugins
• decide which framework and browsers to run on
• setup preprocessors
• list reporters
• list required helpers, vendors, sources and specs in files
75. TIPS
• setup different config for developing and CI
developing: runs on different browsers
CI: runs on phantomjs with coverage report
• if grunt-watch is exploited, it’s better to disable
watch feature of karma
i.e each karma run is a single run, and re-run it when file changed
76. THANKYOU
w3 mse spec, media event sample
MDN media element doc, HTMLMediaElement doc
dash.js
Angular.js Directive doc
React.js Getting Start tutorial
ref: