Commit aa016e28 by Calen Pennington

Merge pull request #82 from MITx/ps-fix-player

Fix Youtube player glitches on iOS
parents 7cf4b159 c380bd82
...@@ -11,12 +11,8 @@ jasmine.stubbedMetadata = ...@@ -11,12 +11,8 @@ jasmine.stubbedMetadata =
duration: 300 duration: 300
jasmine.stubbedCaption = jasmine.stubbedCaption =
start: [0, 10000, 20000, 30000, 40000, 50000, 60000, 70000, 80000, 90000, start: [0, 10000, 20000, 30000]
100000, 110000, 120000] text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', 'Caption at 30000']
text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000',
'Caption at 30000', 'Caption at 40000', 'Caption at 50000', 'Caption at 60000',
'Caption at 70000', 'Caption at 80000', 'Caption at 90000', 'Caption at 100000',
'Caption at 110000', 'Caption at 120000']
jasmine.stubRequests = -> jasmine.stubRequests = ->
spyOn($, 'ajax').andCallFake (settings) -> spyOn($, 'ajax').andCallFake (settings) ->
......
...@@ -9,6 +9,9 @@ describe 'VideoCaption', -> ...@@ -9,6 +9,9 @@ describe 'VideoCaption', ->
describe 'constructor', -> describe 'constructor', ->
beforeEach -> beforeEach ->
spyOn($, 'getWithPrefix').andCallThrough() spyOn($, 'getWithPrefix').andCallThrough()
describe 'always', ->
beforeEach ->
@caption = new VideoCaption @player, 'def456' @caption = new VideoCaption @player, 'def456'
it 'set the player', -> it 'set the player', ->
...@@ -26,21 +29,39 @@ describe 'VideoCaption', -> ...@@ -26,21 +29,39 @@ describe 'VideoCaption', ->
it 'fetch the caption', -> it 'fetch the caption', ->
expect($.getWithPrefix).toHaveBeenCalledWith @caption.captionURL(), jasmine.any(Function) expect($.getWithPrefix).toHaveBeenCalledWith @caption.captionURL(), jasmine.any(Function)
it 'bind window resize event', ->
expect($(window)).toHandleWith 'resize', @caption.onWindowResize
it 'bind player resize event', ->
expect($(@player)).toHandleWith 'resize', @caption.onWindowResize
it 'bind player updatePlayTime event', ->
expect($(@player)).toHandleWith 'updatePlayTime', @caption.onUpdatePlayTime
it 'bind player play event', ->
expect($(@player)).toHandleWith 'play', @caption.onPlay
it 'bind the hide caption button', ->
expect($('.hide-subtitles')).toHandleWith 'click', @caption.toggle
it 'bind the mouse movement', ->
expect($('.subtitles')).toHandleWith 'mouseenter', @caption.onMouseEnter
expect($('.subtitles')).toHandleWith 'mouseleave', @caption.onMouseLeave
expect($('.subtitles')).toHandleWith 'mousemove', @caption.onMovement
expect($('.subtitles')).toHandleWith 'mousewheel', @caption.onMovement
expect($('.subtitles')).toHandleWith 'DOMMouseScroll', @caption.onMovement
describe 'when on a non touch-based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
@caption = new VideoCaption @player, 'def456'
it 'render the caption', -> it 'render the caption', ->
expect($('.subtitles').html()).toMatch new RegExp(''' expect($('.subtitles').html()).toMatch new RegExp('''
<li data-index="0" data-start="0">Caption at 0</li> <li data-index="0" data-start="0">Caption at 0</li>
<li data-index="1" data-start="10000">Caption at 10000</li> <li data-index="1" data-start="10000">Caption at 10000</li>
<li data-index="2" data-start="20000">Caption at 20000</li> <li data-index="2" data-start="20000">Caption at 20000</li>
<li data-index="3" data-start="30000">Caption at 30000</li> <li data-index="3" data-start="30000">Caption at 30000</li>
<li data-index="4" data-start="40000">Caption at 40000</li>
<li data-index="5" data-start="50000">Caption at 50000</li>
<li data-index="6" data-start="60000">Caption at 60000</li>
<li data-index="7" data-start="70000">Caption at 70000</li>
<li data-index="8" data-start="80000">Caption at 80000</li>
<li data-index="9" data-start="90000">Caption at 90000</li>
<li data-index="10" data-start="100000">Caption at 100000</li>
<li data-index="11" data-start="110000">Caption at 110000</li>
<li data-index="12" data-start="120000">Caption at 120000</li>
'''.replace(/\n/g, '')) '''.replace(/\n/g, ''))
it 'add a padding element to caption', -> it 'add a padding element to caption', ->
...@@ -51,24 +72,19 @@ describe 'VideoCaption', -> ...@@ -51,24 +72,19 @@ describe 'VideoCaption', ->
$('.subtitles li[data-index]').each (index, link) => $('.subtitles li[data-index]').each (index, link) =>
expect($(link)).toHandleWith 'click', @caption.seekPlayer expect($(link)).toHandleWith 'click', @caption.seekPlayer
it 'bind window resize event', -> it 'set rendered to true', ->
expect($(window)).toHandleWith 'resize', @caption.onWindowResize expect(@caption.rendered).toBeTruthy()
it 'bind player resize event', ->
expect($(@player)).toHandleWith 'resize', @caption.onWindowResize
it 'bind player updatePlayTime event', -> describe 'when on a touch-based device', ->
expect($(@player)).toHandleWith 'updatePlayTime', @caption.onUpdatePlayTime beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true
@caption = new VideoCaption @player, 'def456'
it 'bind the hide caption button', -> it 'show explaination message', ->
expect($('.hide-subtitles')).toHandleWith 'click', @caption.toggle expect($('.subtitles li')).toHaveHtml "Caption will be displayed when you start playing the video."
it 'bind the mouse movement', -> it 'does not set rendered to true', ->
expect($('.subtitles')).toHandleWith 'mouseenter', @caption.onMouseEnter expect(@caption.rendered).toBeFalsy()
expect($('.subtitles')).toHandleWith 'mouseleave', @caption.onMouseLeave
expect($('.subtitles')).toHandleWith 'mousemove', @caption.onMovement
expect($('.subtitles')).toHandleWith 'mousewheel', @caption.onMovement
expect($('.subtitles')).toHandleWith 'DOMMouseScroll', @caption.onMovement
describe 'mouse movement', -> describe 'mouse movement', ->
beforeEach -> beforeEach ->
...@@ -145,8 +161,34 @@ describe 'VideoCaption', -> ...@@ -145,8 +161,34 @@ describe 'VideoCaption', ->
expect(@caption.search(9999)).toEqual 0 expect(@caption.search(9999)).toEqual 0
expect(@caption.search(10000)).toEqual 1 expect(@caption.search(10000)).toEqual 1
expect(@caption.search(15000)).toEqual 1 expect(@caption.search(15000)).toEqual 1
expect(@caption.search(120000)).toEqual 12 expect(@caption.search(30000)).toEqual 3
expect(@caption.search(120001)).toEqual 12 expect(@caption.search(30001)).toEqual 3
describe 'onPlay', ->
describe 'when the caption was not rendered', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true
@caption = new VideoCaption @player, 'def456'
@caption.onPlay()
it 'render the caption', ->
expect($('.subtitles').html()).toMatch new RegExp('''
<li data-index="0" data-start="0">Caption at 0</li>
<li data-index="1" data-start="10000">Caption at 10000</li>
<li data-index="2" data-start="20000">Caption at 20000</li>
<li data-index="3" data-start="30000">Caption at 30000</li>
'''.replace(/\n/g, ''))
it 'add a padding element to caption', ->
expect($('.subtitles li:first')).toBe '.spacing'
expect($('.subtitles li:last')).toBe '.spacing'
it 'bind all the caption link', ->
$('.subtitles li[data-index]').each (index, link) =>
expect($(link)).toHandleWith 'click', @caption.seekPlayer
it 'set rendered to true', ->
expect(@caption.rendered).toBeTruthy()
describe 'onUpdatePlayTime', -> describe 'onUpdatePlayTime', ->
beforeEach -> beforeEach ->
......
describe 'VideoControl', -> describe 'VideoControl', ->
beforeEach -> beforeEach ->
@player = jasmine.stubVideoPlayer @ @player = jasmine.stubVideoPlayer @
$('.video-controls').html ''
describe 'constructor', -> describe 'constructor', ->
beforeEach ->
@control = new VideoControl @player
it 'render the video controls', -> it 'render the video controls', ->
new VideoControl @player
expect($('.video-controls').html()).toContain ''' expect($('.video-controls').html()).toContain '''
<div class="slider"></div> <div class="slider"></div>
<div> <div>
<ul class="vcr"> <ul class="vcr">
<li><a class="video_control play">Play</a></li> <li><a class="video_control play" href="#">Play</a></li>
<li> <li>
<div class="vidtime">0:00 / 0:00</div> <div class="vidtime">0:00 / 0:00</div>
</li> </li>
...@@ -23,12 +22,33 @@ describe 'VideoControl', -> ...@@ -23,12 +22,33 @@ describe 'VideoControl', ->
''' '''
it 'bind player events', -> it 'bind player events', ->
expect($(@player)).toHandleWith 'play', @control.onPlay control = new VideoControl @player
expect($(@player)).toHandleWith 'pause', @control.onPause expect($(@player)).toHandleWith 'play', control.onPlay
expect($(@player)).toHandleWith 'ended', @control.onPause expect($(@player)).toHandleWith 'pause', control.onPause
expect($(@player)).toHandleWith 'ended', control.onPause
it 'bind the playback button', -> it 'bind the playback button', ->
expect($('.video_control')).toHandleWith 'click', @control.togglePlayback control = new VideoControl @player
expect($('.video_control')).toHandleWith 'click', control.togglePlayback
describe 'when on a touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true
it 'does not add the play class to video control', ->
new VideoControl @player
expect($('.video_control')).not.toHaveClass 'play'
expect($('.video_control')).not.toHaveHtml 'Play'
describe 'when on a non-touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
it 'add the play class to video control', ->
new VideoControl @player
expect($('.video_control')).toHaveClass 'play'
expect($('.video_control')).toHaveHtml 'Play'
describe 'onPlay', -> describe 'onPlay', ->
beforeEach -> beforeEach ->
...@@ -54,6 +74,33 @@ describe 'VideoControl', -> ...@@ -54,6 +74,33 @@ describe 'VideoControl', ->
beforeEach -> beforeEach ->
@control = new VideoControl @player @control = new VideoControl @player
describe 'when the control does not have play or pause class', ->
beforeEach ->
$('.video_control').removeClass('play').removeClass('pause')
describe 'when the video is playing', ->
beforeEach ->
spyOn(@player, 'isPlaying').andReturn true
spyOnEvent @player, 'pause'
@control.togglePlayback jQuery.Event('click')
it 'does not trigger the pause event', ->
expect('pause').not.toHaveBeenTriggeredOn @player
describe 'when the video is paused', ->
beforeEach ->
spyOn(@player, 'isPlaying').andReturn false
spyOnEvent @player, 'play'
@control.togglePlayback jQuery.Event('click')
it 'does not trigger the play event', ->
expect('play').not.toHaveBeenTriggeredOn @player
for className in ['play', 'pause']
describe "when the control has #{className} class", ->
beforeEach ->
$('.video_control').addClass className
describe 'when the video is playing', -> describe 'when the video is playing', ->
beforeEach -> beforeEach ->
spyOn(@player, 'isPlaying').andReturn true spyOn(@player, 'isPlaying').andReturn true
......
...@@ -90,7 +90,7 @@ describe 'VideoPlayer', -> ...@@ -90,7 +90,7 @@ describe 'VideoPlayer', ->
describe 'when not on a touch based device', -> describe 'when not on a touch based device', ->
beforeEach -> beforeEach ->
window.onTouchBasedDevice = -> false spyOn(window, 'onTouchBasedDevice').andReturn false
spyOn @player, 'play' spyOn @player, 'play'
@player.onReady() @player.onReady()
...@@ -99,7 +99,7 @@ describe 'VideoPlayer', -> ...@@ -99,7 +99,7 @@ describe 'VideoPlayer', ->
describe 'when on a touch based device', -> describe 'when on a touch based device', ->
beforeEach -> beforeEach ->
window.onTouchBasedDevice = -> true spyOn(window, 'onTouchBasedDevice').andReturn true
spyOn @player, 'play' spyOn @player, 'play'
@player.onReady() @player.onReady()
......
...@@ -3,8 +3,10 @@ describe 'VideoProgressSlider', -> ...@@ -3,8 +3,10 @@ describe 'VideoProgressSlider', ->
@player = jasmine.stubVideoPlayer @ @player = jasmine.stubVideoPlayer @
describe 'constructor', -> describe 'constructor', ->
describe 'on a non-touch based device', ->
beforeEach -> beforeEach ->
spyOn($.fn, 'slider').andCallThrough() spyOn($.fn, 'slider').andCallThrough()
spyOn(window, 'onTouchBasedDevice').andReturn false
@slider = new VideoProgressSlider @player @slider = new VideoProgressSlider @player
it 'build the slider', -> it 'build the slider', ->
...@@ -31,6 +33,21 @@ describe 'VideoProgressSlider', -> ...@@ -31,6 +33,21 @@ describe 'VideoProgressSlider', ->
it 'bind player events', -> it 'bind player events', ->
expect($(@player)).toHandleWith 'updatePlayTime', @slider.onUpdatePlayTime expect($(@player)).toHandleWith 'updatePlayTime', @slider.onUpdatePlayTime
expect($(@player)).toHandleWith 'ready', @slider.onReady
expect($(@player)).toHandleWith 'play', @slider.onPlay
describe 'on a touch-based device', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
spyOn(window, 'onTouchBasedDevice').andReturn true
@slider = new VideoProgressSlider @player
it 'does not build the slider', ->
expect(@slider.slider).toBeUndefined
expect($.fn.slider).not.toHaveBeenCalled()
it 'bind player events', ->
expect($(@player)).toHandleWith 'updatePlayTime', @slider.onUpdatePlayTime
describe 'onReady', -> describe 'onReady', ->
beforeEach -> beforeEach ->
...@@ -41,6 +58,45 @@ describe 'VideoProgressSlider', -> ...@@ -41,6 +58,45 @@ describe 'VideoProgressSlider', ->
it 'set the max value to the length of video', -> it 'set the max value to the length of video', ->
expect(@slider.slider.slider('option', 'max')).toEqual 120 expect(@slider.slider.slider('option', 'max')).toEqual 120
describe 'onPlay', ->
beforeEach ->
@slider = new VideoProgressSlider @player
spyOn($.fn, 'slider').andCallThrough()
describe 'when the slider was already built', ->
beforeEach ->
@slider.onPlay()
it 'does not build the slider', ->
expect($.fn.slider).not.toHaveBeenCalled
describe 'when the slider was not already built', ->
beforeEach ->
@slider.slider = null
@slider.onPlay()
it 'build the slider', ->
expect(@slider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith
range: 'min'
change: @slider.onChange
slide: @slider.onSlide
stop: @slider.onStop
it 'build the seek handle', ->
expect(@slider.handle).toBe '.ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00"
position:
my: 'bottom center'
at: 'top center'
container: @slider.handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
describe 'onUpdatePlayTime', -> describe 'onUpdatePlayTime', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider @player @slider = new VideoProgressSlider @player
......
...@@ -10,6 +10,7 @@ class @VideoCaption ...@@ -10,6 +10,7 @@ class @VideoCaption
$(window).bind('resize', @onWindowResize) $(window).bind('resize', @onWindowResize)
$(@player).bind('resize', @onWindowResize) $(@player).bind('resize', @onWindowResize)
$(@player).bind('updatePlayTime', @onUpdatePlayTime) $(@player).bind('updatePlayTime', @onUpdatePlayTime)
$(@player).bind('play', @onPlay)
@$('.hide-subtitles').click @toggle @$('.hide-subtitles').click @toggle
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave) @$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
.mousemove(@onMovement).bind('mousewheel', @onMovement) .mousemove(@onMovement).bind('mousewheel', @onMovement)
...@@ -32,6 +33,10 @@ class @VideoCaption ...@@ -32,6 +33,10 @@ class @VideoCaption
$.getWithPrefix @captionURL(), (captions) => $.getWithPrefix @captionURL(), (captions) =>
@captions = captions.text @captions = captions.text
@start = captions.start @start = captions.start
if onTouchBasedDevice()
$('.subtitles li').html "Caption will be displayed when you start playing the video."
else
@renderCaption() @renderCaption()
renderCaption: -> renderCaption: ->
...@@ -49,6 +54,8 @@ class @VideoCaption ...@@ -49,6 +54,8 @@ class @VideoCaption
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight())) @$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
.append($('<li class="spacing">').height(@bottomSpacingHeight())) .append($('<li class="spacing">').height(@bottomSpacingHeight()))
@rendered = true
search: (time) -> search: (time) ->
min = 0 min = 0
max = @start.length - 1 max = @start.length - 1
...@@ -62,6 +69,9 @@ class @VideoCaption ...@@ -62,6 +69,9 @@ class @VideoCaption
return min return min
onPlay: =>
@renderCaption() unless @rendered
onUpdatePlayTime: (event, time) => onUpdatePlayTime: (event, time) =>
# This 250ms offset is required to match the video speed # This 250ms offset is required to match the video speed
time = Math.round(Time.convert(time, @player.currentSpeed(), '1.0') * 1000 + 250) time = Math.round(Time.convert(time, @player.currentSpeed(), '1.0') * 1000 + 250)
......
...@@ -17,7 +17,7 @@ class @VideoControl ...@@ -17,7 +17,7 @@ class @VideoControl
<div class="slider"></div> <div class="slider"></div>
<div> <div>
<ul class="vcr"> <ul class="vcr">
<li><a class="video_control play">Play</a></li> <li><a class="video_control" href="#"></a></li>
<li> <li>
<div class="vidtime">0:00 / 0:00</div> <div class="vidtime">0:00 / 0:00</div>
</li> </li>
...@@ -28,6 +28,9 @@ class @VideoControl ...@@ -28,6 +28,9 @@ class @VideoControl
</div> </div>
""" """
unless onTouchBasedDevice()
@$('.video_control').addClass('play').html('Play')
onPlay: => onPlay: =>
@$('.video_control').removeClass('play').addClass('pause').html('Pause') @$('.video_control').removeClass('play').addClass('pause').html('Pause')
...@@ -36,6 +39,7 @@ class @VideoControl ...@@ -36,6 +39,7 @@ class @VideoControl
togglePlayback: (event) => togglePlayback: (event) =>
event.preventDefault() event.preventDefault()
if $('.video_control').hasClass('play') || $('.video_control').hasClass('pause')
if @player.isPlaying() if @player.isPlaying()
$(@player).trigger('pause') $(@player).trigger('pause')
else else
......
class @VideoProgressSlider class @VideoProgressSlider
constructor: (@player) -> constructor: (@player) ->
@buildSlider() @buildSlider() unless onTouchBasedDevice()
@buildHandle()
$(@player).bind('updatePlayTime', @onUpdatePlayTime) $(@player).bind('updatePlayTime', @onUpdatePlayTime)
$(@player).bind('ready', @onReady) $(@player).bind('ready', @onReady)
$(@player).bind('play', @onPlay)
$: (selector) -> $: (selector) ->
@player.$(selector) @player.$(selector)
...@@ -14,6 +14,7 @@ class @VideoProgressSlider ...@@ -14,6 +14,7 @@ class @VideoProgressSlider
change: @onChange change: @onChange
slide: @onSlide slide: @onSlide
stop: @onStop stop: @onStop
@buildHandle()
buildHandle: -> buildHandle: ->
@handle = @$('.ui-slider-handle') @handle = @$('.ui-slider-handle')
...@@ -30,10 +31,13 @@ class @VideoProgressSlider ...@@ -30,10 +31,13 @@ class @VideoProgressSlider
widget: true widget: true
onReady: => onReady: =>
@slider.slider('option', 'max', @player.duration()) @slider.slider('option', 'max', @player.duration()) if @slider
onPlay: =>
@buildSlider() unless @slider
onUpdatePlayTime: (event, currentTime) => onUpdatePlayTime: (event, currentTime) =>
if !@frozen if @slider && !@frozen
@slider.slider('option', 'max', @player.duration()) @slider.slider('option', 'max', @player.duration())
@slider.slider('value', currentTime) @slider.slider('value', currentTime)
......
...@@ -137,6 +137,7 @@ section.course-content { ...@@ -137,6 +137,7 @@ section.course-content {
float: left; float: left;
margin-bottom: 0; margin-bottom: 0;
a { a {
border-bottom: none; border-bottom: none;
border-right: 1px solid #000; border-right: 1px solid #000;
...@@ -146,11 +147,17 @@ section.course-content { ...@@ -146,11 +147,17 @@ section.course-content {
line-height: 46px; line-height: 46px;
padding: 0 lh(.75); padding: 0 lh(.75);
text-indent: -9999px; text-indent: -9999px;
@include transition(); @include transition(background-color, opacity);
width: 14px; width: 14px;
background: url('../images/vcr.png') 15px 15px no-repeat;
&:empty {
height: 46px;
background: url('../images/vcr.png') 15px 15px no-repeat;
}
&.play { &.play {
background: url('../images/play-icon.png') center center no-repeat; background-position: 17px -114px;
&:hover { &:hover {
background-color: #444; background-color: #444;
...@@ -158,7 +165,7 @@ section.course-content { ...@@ -158,7 +165,7 @@ section.course-content {
} }
&.pause { &.pause {
background: url('../images/pause-icon.png') center center no-repeat; background-position: 16px -50px;
&:hover { &:hover {
background-color: #444; background-color: #444;
...@@ -361,7 +368,6 @@ section.course-content { ...@@ -361,7 +368,6 @@ section.course-content {
cursor: pointer; cursor: pointer;
margin-bottom: 8px; margin-bottom: 8px;
padding: 0; padding: 0;
@include transition(all, .5s, ease-in);
&.current { &.current {
color: #333; color: #333;
...@@ -386,7 +392,6 @@ section.course-content { ...@@ -386,7 +392,6 @@ section.course-content {
} }
ol.subtitles { ol.subtitles {
height: 0;
width: 0px; width: 0px;
} }
} }
...@@ -408,7 +413,6 @@ section.course-content { ...@@ -408,7 +413,6 @@ section.course-content {
&.closed { &.closed {
ol.subtitles { ol.subtitles {
height: auto;
right: -(flex-grid(4)); right: -(flex-grid(4));
width: auto; width: auto;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment