Commit c412b7dc authored by Michael Murtaugh's avatar Michael Murtaugh
Browse files

media sync

parent efbcde4f
......@@ -7,276 +7,128 @@
<script src="../lib/jquery/jquery-1.7.1.min.js"></script>
<script src="../lib/jquery/jquery-ui-1.8.17.custom.min.js"></script>
<script src="../source/jquery.voidplayer_simple.js"></script>
<script src="../source/jquery.datetimecode.js"></script>
<script src="../source/jquery.timeline.js"></script>
<script src="../source/jquery.aamediasync.js"></script>
<link rel="stylesheet" href="../lib/jquery/css/ui-lightness/jquery-ui-1.8.17.custom.css" />
<script>
$(document).ready(function () {
// RINGING: audio sets video sets audio...
// SOLUTION: ONLY ALLOW "SYNCING" FROM ONE SOURCE AT A TIME
// ENTER into a state of FOLLOWING a particular time source ...
// ... follow it, setting the rest from that info
// ... EXIT the state when seeking is complete
// NEXT STEP
// MANAGE PLAY STATES
// on SCRUB: all elements to pause
// end of seek... wait for all elements to be ready => then play (when playing)
// HEY! is not dragging state a form of driver ! :)
function when_all_ready (callback) {
// console.log("cleardriver");
if ((audio.readyState == 4 && !audio.seeking) && (video.readyState == 4 && !video.seeking)) {
callback();
} else {
window.setTimeout(function () { when_all_ready(callback) }, 100);
}
<style>
.annotation {
font-size: 11px;
border: 1px solid black;
width: 320px;
height: 360px;
overflow: auto;
margin-bottom: 10px;
}
.title {
border-top: 1px dotted black;
padding: 4px;
cursor: pointer;
}
.active {
background: pink;
}
td {
vertical-align: top;
}
.timecodes {
font-family: monospace;
font-size: 10px;
color: gray;
cursor: pointer;
}
</style>
///////////TIMELINE
<script>
/*
*/
$(document).ready(function () {
var timeline_duration = 60 * 60,
video_start_time = 1 * 60,
audio_start_time = 2 * 60,
video_start_time = 5, // 1 * 60,
audio_start_time = 10,
media_duration = 5*60,
slider_max = 100000,
audio = $("audio").get(0),
video = $("video").get(0),
timeline = new VoidPlayer($("#timeline").get(0)),
slider_max = 100000,
driver = null,
driver_scrubstart_paused;
timeline = new VoidPlayer(document.getElementById("timeline"));
var sync = $.aaMediaSync(timeline, {trace: false});
sync.add(audio, audio_start_time, audio_start_time + media_duration);
sync.add(video, video_start_time, video_start_time + media_duration);
$("#timeline").timeline({
currentTime: function (elt) {
return timeline.currentTime;
},
show: function (elt) {
// console.log("show", elt);
$(elt).show();
sync.enter(elt);
/*
if (!timeline.paused) {
// console.log("trigger group play");
timeline.pause();
timeline.play();
}
*/
},
hide: function (elt) {
// console.log("hide", elt);
$(elt).hide();
// hide before sync
sync.exit(elt);
}
});
$("#timeline").timeline("add", audio, {
start: audio_start_time,
end: audio_start_time + media_duration
}).timeline("add", video, {
start: video_start_time,
end: video_start_time + media_duration
});
////////////////////////////
// "OUTSIDE" CONTROLS CODE
// Slider & button state follows #timeline events (timeupdate, play, pause)
$("#timeline").bind("timeupdate", function () {
// console.log("timeline timeupdate", timeline.currentTime);
// Update the slider based on currentTime/duration
var slider_val = (timeline.currentTime / timeline.duration) * slider_max;
$("#timeline").slider("option", "value", slider_val);
$("#timedisplay").text(timeline.currentTime.secondsTo("mm:ss"));
}).bind("play", function () {
$("#play").text("pause");
var playbutton = $("#play").text("pause");
// playbutton.disabled = false;
}).bind("pause", function () {
$("#play").text("play");
});
// timeline controls
$("#play").click(function () {
if (timeline.paused) {
var playbutton = this;
playbutton.disabled = true;
when_all_ready(function () {
timeline.play();
video.play();
audio.play();
playbutton.disabled = false;
});
} else {
timeline.pause();
video.pause();
audio.pause();
}
timeline.paused ? timeline.play() : timeline.pause();
});
$("#timeline").slider({
start: function (evt, ui) {
if (driver === null) {
driver = timeline;
// RECORD PLAY STATE
driver_scrubstart_paused = timeline.paused;
// PAUSE ALL
timeline.pause();
audio.pause();
video.pause();
}
},
slide: function (evt, ui) {
// console.log("slide", ui.value);
var ct = timeline.duration * (ui.value/slider_max);
timeline.setCurrentTime(ct);
if (ct > audio_start_time) {
audio.currentTime = ct - audio_start_time;
}
if (ct > video_start_time) {
video.currentTime = ct - video_start_time;
}
},
stop: function (evt, ui) {
if (driver == timeline) {
// driver = null;
when_all_ready(function () {
driver = null;
// restore play state
if (!driver_scrubstart_paused) {
timeline.play();
video.play();
audio.play();
}
});
}
},
max: slider_max
});
///////////////////////////////
// VIDEO
$("video").bind("seeking", function () {
// console.log("video, seeking");
if (driver === null) {
driver = video;
// RECORD PLAY STATE
driver_scrubstart_paused = video.paused;
// PAUSE ALL
timeline.pause();
audio.pause();
video.pause();
}
}).bind("seeked", function () {
// PROBLEM FOR CHROME: canplay does not fire on seeking...
// console.log("video, canplay");
// PROBLEM for firefox seeked fires a bit early
if (driver === video) {
// wait until all media is ready to set driver to null
// otherwise slower media (video) can trigger more "scrub" events
when_all_ready(function () {
driver = null;
// restore play state
if (!driver_scrubstart_paused) {
// console.log("restore play state video");
timeline.play();
video.play();
audio.play();
}
});
}
}).bind("timeupdate", function () {
// console.log("timeupdate", this);
if (driver === video) {
// Video is being scrubbed (timeupdate while buffering = scrub event ?!)
console.log("video scrub " + video.currentTime);
// set timeline time
timeline.setCurrentTime(video_start_time + this.currentTime);
// set audio time
var audio_time = (timeline.currentTime - audio_start_time);
if (audio_time >= 0) {
audio.currentTime = audio_time;
}
}
}).bind("pause", function () {
// console.log("video: pause");
});
///////////////////////////////
// AUDIO
$("audio").bind("seeking", function () {
if (driver === null) {
driver = audio;
driver_scrubstart_paused = audio.paused;
// PAUSE ALL
// console.log("pausing all media, scrubstart_paused", driver_scrubstart_paused);
timeline.pause();
audio.pause();
video.pause();
}
}).bind("seeked", function () {
// console.log("audio seeked", driver_scrubstart_paused);
if (driver === audio) {
// driver = null;
audio.pause(); // HACK TO OVERRIDE (firefox) scrub resuming play
when_all_ready(function () {
// console.log("driver = null", driver_scrubstart_paused);
driver = null;
// restore play state
if (!driver_scrubstart_paused) {
// console.log("restore play state audio");
timeline.play();
video.play();
audio.play();
}
});
}
}).bind("timeupdate", function () {
if (driver === audio) {
// SCRUB (timeupdate while buffering = scrub event ?!)
console.log("audio scrub " + audio.currentTime);
// set timeline time
timeline.setCurrentTime(audio_start_time + this.currentTime);
// set video time
var video_time = (timeline.currentTime - video_start_time);
if (video_time >= 0) {
video.currentTime = video_time;
}
}
}).bind("play", function () {
// if received "cold" (driver === null)
// initiate a group play... pause this, and when_all_ready set all playstates to play
// then wait for all play events to be received to complete the "group play" event
// once the "group play" event has been received, pause/play events are swallowed / handled differently
if (driver === null) {
console.log("initiate play when ready");
driver = audio;
audio.pause();
when_all_ready(function () {
console.log("ready, calling play");
audio.play();
video.play();
timeline.play();
});
} else if (driver === audio) {
window.setTimeout(function () {
console.log("setting driver to null");
driver = null;
}, 50);
}
}).bind("pause", function () {
// console.log("audio: pause");
if (driver === null) {
video.pause();
timeline.pause();
}
});
// Clickable annotation timecodes
$("div.annotation [data-start]").each(function () {
var secs = $(this).attr("data-start").toSeconds();
$(this).click(function () {
audio.currentTime = secs;
video.currentTime = secs;
video.play();
});
});
});
</script>
});
<style>
.annotation {
font-size: 11px;
border: 1px solid black;
width: 320px;
height: 360px;
overflow: auto;
margin-bottom: 10px;
}
.title {
border-top: 1px dotted black;
padding: 4px;
}
.section {
border-top: 1px dotted black;
padding: 4px;
}
.active {
background: pink;
}
td {
vertical-align: top;
}
.timecodes {
font-family: monospace;
font-size: 10px;
color: gray;
cursor: pointer;
}
</style>
</script>
</head>
<body>
......@@ -288,13 +140,13 @@ td {
<div>
<span id="timedisplay"></span>
<button id="play">play</button>
<button id="lock">locked</button>
</div>
<table>
<tr>
<td>
<video controls src="/videos/Michael_Moss.ogv" data-start="00:00:05"></video>
<!-- <video controls src="/videos/Michael_Moss.ogv" data-start="00:00:05"></video>-->
<video controls src="http://video.constantvzw.org/vj12/Michael_Moss.ogv" data-start="00:00:05"></video>
<audio controls src="Jonathan Burrows on scores.ogg" data-start="00:00:05"></audio>
</td>
<td>
......
(function ($) {
//////////////////////////////
// SYNCING CODE
// var when_all_ready_callbacks = [];
// SEEKING: Become the driver (if no one else is already), remember my playstate, pause all
// SEEKED: If I am the driver, (pause yourself -- ff hack), when_all_ready: relinquish driver role, play all (if I was playing when seeking began)
// TIMEUPDATE: If I am the driver, this is a "scrub": Set other elements' currentTime accordingly
// PLAY: Become the driver (if no one else is already), (pause self -- ff hack?): when_all_ready: play all -- relinquish driver role (with some delay!?)
// allow request for play to work together with a scrub
function aaMediaSync (element, opts) {
var that = {element: element},
syncedmedia = [element],
uid = 0,
driver = null,
initiatingGroupPlay = false,
driver_scrubstart_paused = undefined;
opts = $.extend({
trace: false
}, opts);
bindevents(element);
function remove (array, elt) {
for (var i=0, l=array.length; i<l; i++) {
if (array[i] === elt) { array.splice(i, 1); return i; }
}
}
function add(elt, start, end) {
var id = ++uid;
syncedmedia.push(elt);
// console.log("aamediasync.add", elt, start, end);
$(elt).data("aamediasync", {start: start, end: end});
bindevents(elt);
}
that.add = add;
function get_all_ready () {
var i, l, m;
for (i=0, l=syncedmedia.length; i<l; i++) {
m = syncedmedia[i];
if (!is_visible(m)) { continue; }
if (! ((m.readyState >= 3) && !m.seeking) ) {
return false;
}
}
return true;
}
function when_all_ready (callback) {
// abstract me (use syncedmedia array)
// Does this need to be written to work with the EVENTS SYSTEM (to stay properly in sync ?!)
if (opts.trace) console.log("when_all_ready", syncedmedia.length);
if (get_all_ready() === true) { callback(); } else {
window.setTimeout(function () { when_all_ready(callback) }, 100);
// when_all_ready_callbacks.push(callback);
}
}
function get_start_time (elt) {
if (elt === element)
return 0;
else {
var data = $(elt).data("aamediasync");
return data.start || 0;
}
}
function setCurrentTime(elt, t) {
// if (!is_visible(elt)) return;
if (elt.setCurrentTime) {
elt.setCurrentTime(t);
} else {
elt.currentTime = t;
}
}
function getRelativeTime(ref, forElt) {
// ref.currentTime is used as reference forElt
// returns currentTime for forElt, given ref.currentTime
var reftime = (ref === syncedmedia[0]) ? ref.currentTime : ref.currentTime + get_start_time(ref);
return (reftime - get_start_time(forElt));
}
function bind (elt, msg, callback) {
if (elt.element !== undefined) {
// Bind to DOM element, Arrange for callback to have the JS object as this (a.l.d. element)
$(elt.element).bind(msg, function () {
callback.call(elt)
});
} else {
// Bind as normal
$(elt).bind(msg, callback);
}
}
function is_visible (elt) {
if (elt.element !== undefined) {
return $(elt.element).is(":visible");
} else {
return $(elt).is(":visible");
}
}
function enter (elt) {
// sync elt time to timeline
var rt = getRelativeTime(element, elt);
if (opts.trace) console.log("enter", elt, rt);
setCurrentTime(elt, rt);
syncedmedia.push(elt);
if (!element.paused) {
// if (trace) console.log("enter: trigger group play");
element.pause();
element.play();
}
}
that.enter = enter;
function exit (elt) {
if (opts.trace) console.log("exit");
remove(syncedmedia, elt);
elt.pause();
}
that.exit = exit;
function groupplay (triggering_elt) {
if (opts.trace) console.log("initiate groupplay");
initiatingGroupPlay = true;
if (triggering_elt) triggering_elt.pause();
when_all_ready(function () {
if (opts.trace) console.log("ready, calling play");
// timeline.play();
$(syncedmedia).each(function () {
if (is_visible(this)) {
this.play()
}
});
window.setTimeout(function () {
initiatingGroupPlay = false;
}, 1000);
});
}
function bindevents (elt) {
bind(elt, "seeking", function () {
if (opts.trace) console.log("seeking", this);
if (driver === null) {
driver = this;
driver_scrubstart_paused = this.paused;
// PAUSE ALL
// timeline.pause();
$(syncedmedia).each(function () { this.pause(); });
}
});
bind(elt, "seeked", function () {
if (driver === this) {
// driver = null;
this.pause(); // HACK TO OVERRIDE (firefox) scrub resuming play
when_all_ready(function () {
// console.log("driver = null", driver_scrubstart_paused);
driver = null;
// restore play state
if (!driver_scrubstart_paused) {
// console.log("restore play state audio");
// timeline.play();
$(syncedmedia).each(function () {
if (is_visible(this)) { this.play(); }
});
}
});
}
})
bind(elt, "timeupdate", function () {
var media = $(this).data("media");
if (driver === this) {
// SCRUB (timeupdate while buffering = scrub event ?!)
if (opts.trace) console.log("scrub", this.currentTime, this);
// SET OTHER TIMES BASED OFF OF THIS ELEMENT
//timeline.setCurrentTime(get_start_time(this) + this.currentTime);
var eventreceiver = this;
$(syncedmedia).each(function () {
if (this !== eventreceiver) {
var newtime = getRelativeTime(eventreceiver, this);
// console.log("getRelativeTime", eventreceiver, this, newtime);
setCurrentTime(this, newtime);
// var time = timeline.currentTime - get_start_time(this);
// if (time >= 0) { this.currentTime = time; }
}
});
}
});
bind(elt, "play", function () {
if (initiatingGroupPlay === false) {
groupplay(this);
/*
if (opts.trace) console.log("initiate groupplay");
initiatingGroupPlay = true;
this.pause();
when_all_ready(function () {
if (opts.trace) console.log("ready, calling play");
// timeline.play();
$(syncedmedia).each(function () {
if (is_visible(this)) {
this.play()
}
});
window.setTimeout(function () {
initiatingGroupPlay = false;
}, 1000);
});
*/
}
});
bind(elt, "pause", function () {
// console.log("audio: pause");
var eventreceiver = this;
if (is_visible(this) && initiatingGroupPlay === false && driver === null) {
// TRIGGER GROUP PAUSE
// timeline.pause();
$(syncedmedia).each(function () {
if (this !== eventreceiver) {
this.pause();
}
});
}
});
}
return that;
}
$.aaMediaSync = aaMediaSync;
})(jQuery);
......@@ -3,7 +3,6 @@
* More information at http://www.gnu.org/licenses/agpl-3.0.html