From f3f40f6e2f43f33866a42f6fb5719d75fc409d19 Mon Sep 17 00:00:00 2001 From: j <0x006A@0x2620.org> Date: Wed, 8 Sep 2010 13:56:58 +0200 Subject: [PATCH] serve frames, timelines and videos, wire up timeline demo --- pandora/app/views.py | 4 + pandora/archive/extract.py | 26 +- pandora/backend/models.py | 23 +- pandora/backend/urls.py | 11 +- pandora/backend/views.py | 89 ++- pandora/settings.py | 13 + pandora/static/css/timeline.css | 181 +++++ pandora/static/js/timeline.js | 1299 +++++++++++++++++++++++++++++++ pandora/templates/timeline.html | 16 + pandora/urls.py | 1 + 10 files changed, 1595 insertions(+), 68 deletions(-) create mode 100644 pandora/static/css/timeline.css create mode 100644 pandora/static/js/timeline.js create mode 100644 pandora/templates/timeline.html diff --git a/pandora/app/views.py b/pandora/app/views.py index 17b84887e..a5a79914f 100644 --- a/pandora/app/views.py +++ b/pandora/app/views.py @@ -16,6 +16,10 @@ def index(request): context = RequestContext(request, {'settings':settings}) return render_to_response('index.html', context) +def timeline(request): + context = RequestContext(request, {'settings':settings}) + return render_to_response('timeline.html', context) + def api_getPage(request): data = json.loads(request.POST['data']) name = data['page'] diff --git a/pandora/archive/extract.py b/pandora/archive/extract.py index 695cb40cd..e7d88ee01 100644 --- a/pandora/archive/extract.py +++ b/pandora/archive/extract.py @@ -194,34 +194,22 @@ def run_command(cmd, timeout=10): killedpid, stat = os.waitpid(p.pid, os.WNOHANG) return p.returncode -def frame(videoFile, position, baseFolder, width=128, redo=False): +def frame(videoFile, frame, position, width=128, redo=False): ''' params: - videoFile + videoFile input + frame output position as float in seconds - baseFolder to write frames to width of frame redo boolean to extract file even if it exists ''' - def frame_path(size): - return os.path.join(baseFolder, "%s.%s.%s" % (ox.ms2time(position*1000), size, img_extension)) - - #not using input file, to slow to extract frame right now - base_size = 320 - frame = frame_path(base_size) - if exists(videoFile): + frameFolder = os.path.dirname(frame) if redo or not exists(frame): - if not exists(baseFolder): - os.makedirs(baseFolder) - cmd = ['oggThumb', '-t', str(position), '-n', frame, '-s', '%dx0'%base_size, videoFile] + if not exists(frameFolder): + os.makedirs(frameFolder) + cmd = ['oxframe', '-i', videoFile, '-o', frame, '-p', str(position), '-x', str(width)] run_command(cmd) - if width != base_size: - frame_base = frame - frame = frame_path(width) - if not exists(frame): - resize_image(frame_base, frame, width) - return frame def resize_image(image_source, image_output, width=None, size=None): if exists(image_source): diff --git a/pandora/backend/models.py b/pandora/backend/models.py index 414f0c7f1..0c42603ae 100644 --- a/pandora/backend/models.py +++ b/pandora/backend/models.py @@ -259,9 +259,11 @@ class Movie(models.Model): self.get('season', ''), self.get('episode', '')) def frame(self, position, width=128): - #FIXME: compute offset and so on - f = self.files.all()[0] - return f.frame(position, width) + stream = self.streams.filter(profile=settings.VIDEO_PROFILE+'.webm')[0] + path = os.path.join(settings.MEDIA_ROOT, 'frame', self.movieId, "%d"%width, "%s.jpg"%position) + if not os.path.exists(path): + extract.frame(stream.video.path, path, position, width) + return path def updateFind(self): try: @@ -411,6 +413,10 @@ class Movie(models.Model): subprocess.Popen(cmd) part += 1 + @property + def timeline_prefix(self): + return os.path.join('stream', movieid_path(self.movieId), 'timeline') + def updateStreams(self): files = {} for f in self.files.filter(is_main=True, video_available=True): @@ -428,11 +434,13 @@ class Movie(models.Model): else: cmd.append('+') cmd.append(files[f]) + if not os.path.exists(os.path.dirname(stream.video.path)): + os.makedirs(os.path.dirname(stream.video.path)) cmd = [ 'mkvmerge', '-o', stream.video.path ] + cmd subprocess.Popen(cmd) stream.save() - extract.timeline(stream.video.path, os.path.join(stream.video.path[:-len(stream.profile)], 'timeline')) + extract.timeline(stream.video.path, os.path.join(settings.MEDIA_ROOT, self.timeline_prefix)) stream.extract_derivatives() #something with poster @@ -704,10 +712,13 @@ class Collection(models.Model): def editable(self, user): return self.users.filter(id=user.id).count() > 0 - + + +def movieid_path(h): + return os.path.join(h[:2], h[2:4], h[4:6], h[6:]) def stream_path(f): h = f.movie.movieId - return os.path.join('stream', h[:2], h[2:4], h[4:6], h[6:], f.profile) + return os.path.join('stream', movieid_path(h), f.profile) class Stream(models.Model): class Meta: diff --git a/pandora/backend/urls.py b/pandora/backend/urls.py index ae98e8aec..9fdaf7fb1 100644 --- a/pandora/backend/urls.py +++ b/pandora/backend/urls.py @@ -5,10 +5,13 @@ from django.conf.urls.defaults import * urlpatterns = patterns("backend.views", - (r'^frame/(?P.*)/(?P.*)\.(?P\d+).jpg$', 'frame'), - (r'^stream/(?P.*).(?P.*).ogv$', 'video'), - (r'^poster/(?P.*)\.(?P\d+)\.jpg$', 'poster'), - (r'^poster/(?P.*)\.jpg$', 'poster'), + (r'^(?P.*)/frame/(?P\d+)/(?P[0-9\.,]+).jpg$', 'frame'), + (r'^(?P.*)/(?P.*.webm)$', 'video'), + (r'^(?P.*)/(?P.*.mp4)$', 'video'), + (r'^(?P.*)/poster\.(?P\d+)\.jpg$', 'poster'), + (r'^(?P.*)/poster\.jpg$', 'poster'), + (r'^(?P.*)/timelines/timeline\.(?P\d+)\.(?P\d+)\.png$', 'timeline'), + (r'^(?P.*)/data/(?P.+)\.json$', 'data'), (r'^api/$', 'api'), ) diff --git a/pandora/backend/views.py b/pandora/backend/views.py index c2a9956f1..a8f845384 100644 --- a/pandora/backend/views.py +++ b/pandora/backend/views.py @@ -494,45 +494,6 @@ def api_getImdbId(request): response = json_response(status=404, text='not found') return render_to_json_response(response) -def poster(request, id, size=None): - print id, size - movie = get_object_or_404(models.Movie, movieId=id) - if movie.poster: - if size: - size = int(size) - poster_path = movie.poster.path.replace('.jpg', '.%d.jpg'%size) - if not os.path.exists(poster_path): - poster_size = max(movie.poster.width, movie.poster.height) - size = min(size, poster_size) - poster_path = movie.poster.path.replace('.jpg', '.%d.jpg'%size) - extract.resize_image(movie.poster.path, poster_path, size=size) - url = movie.poster.url.replace('.jpg', '.%d.jpg'%size) - elif movie.poster: - url = movie.poster.url - else: - url = movie.poster_url - if not url: - url = '/static/png/posterDark.48.png' - return redirect(url) - -def video(request, id, quality): - movie = get_object_or_404(models.Movie, movieId=id) - if quality not in settings.VIDEO_ENCODING: - raise Http404 - stream = getattr(movie, 'stream_'+quality) - response = HttpFileResponse(stream.path, content_type='video/ogg') - #FIXME: movie needs duration field - #response['Content-Duration'] = movie.duration - return response - -def frame(request, id, position, size): - movie = get_object_or_404(models.Movie, movieId=id) - position = ox.time2ms(position)/1000 - frame = movie.frame(position, int(size)) - if not frame: - raise Http404 - return HttpFileResponse(frame, content_type='image/jpeg') - def apidoc(request): ''' this is used for online documentation at http://127.0.0.1:8000/api/ @@ -573,3 +534,53 @@ def apidoc(request): context = RequestContext(request, {'api': api, 'sitename': settings.SITENAME,}) return render_to_response('api.html', context) + +def data(request, id, data): + movie = get_object_or_404(models.Movie, movieId=id) + response = {} + if data == 'video': + response = movie.get_stream() + return render_to_json_response(response) + +#media delivery +def frame(request, id, position, size): + movie = get_object_or_404(models.Movie, movieId=id) + position = float(position.replace(',', '.')) + frame = movie.frame(position, int(size)) + if not frame: + raise Http404 + return HttpFileResponse(frame, content_type='image/jpeg') + +def poster(request, id, size=128): + movie = get_object_or_404(models.Movie, movieId=id) + if size == 'large': + size = None + if movie.poster: + if size: + size = int(size) + poster_path = movie.poster.path.replace('.jpg', '.%d.jpg'%size) + if not os.path.exists(poster_path): + poster_size = max(movie.poster.width, movie.poster.height) + if size > poster_size: + return redirect('/%s/poster.large.jpg' % movie.movieId) + extract.resize_image(movie.poster.path, poster_path, size=size) + else: + poster_path = movie.poster.path + else: + poster_path = os.path.join(settings.STATIC_ROOT, 'png/posterDark.48.png') + return HttpFileResponse(poster_path, content_type='image/jpeg') + +def timeline(request, id, size, position): + movie = get_object_or_404(models.Movie, movieId=id) + timeline = os.path.join(settings.MEDIA_ROOT, '%s.%s.%04d.png' %(movie.timeline_prefix, size, int(position))) + return HttpFileResponse(timeline, content_type='image/png') + +def video(request, id, profile): + movie = get_object_or_404(models.Movie, movieId=id) + stream = get_object_or_404(movie.streams, profile=profile) + path = stream.video.path + content_type = path.endswith('.mp4') and 'video/mp4' or 'video/webm' + #url = 'http://127.0.0.1/pandora_media' + path[len(settings.MEDIA_ROOT):] + #return redirect(url) + return HttpFileResponse(path, content_type=content_type) + diff --git a/pandora/settings.py b/pandora/settings.py index 47ddc2fdc..7a3bb8667 100644 --- a/pandora/settings.py +++ b/pandora/settings.py @@ -14,8 +14,21 @@ DEBUG = True TEMPLATE_DEBUG = DEBUG JSON_DEBUG = True +#with apache x-sendfile or lighttpd set this to True XSENDFILE = False +XACCELREDIRECT = False +# with nginx: +#XACCELREDIRECT=[/some/path/, /protected/] +''' +this assumes the following configuration: +location /protected/ { + internal; + root /some/path/; +} +''' + + ADMINS = ( ('j', 'j@mailb.org'), ) diff --git a/pandora/static/css/timeline.css b/pandora/static/css/timeline.css new file mode 100644 index 000000000..59c818e53 --- /dev/null +++ b/pandora/static/css/timeline.css @@ -0,0 +1,181 @@ +body { + margin: 0; +} +#editor { + margin: 4px; +} +#players { + //background: rgb(255, 192, 192); +} +#timelines { + background: rgb(192, 192, 255); +} + + +.OxEditor .OxVideoPlayer { + position: absolute; + margin: 4px; + //background: red; +} + + + +.OxTimelineLarge { + position: absolute; + height: 72px; + margin: 0 4px 0 4px; + overflow: hidden; +} +.OxTimelineLarge > div { + position: absolute; + height: 72px; +} +.OxTimelineLarge > div > img { + position: absolute; + top: 4px; +} +.OxTimelineLarge .OxCut { + position: absolute; + top: 66px; + width: 2px; + height: 4px; + margin-left: -1px; + z-index: 10; +} +.OxTimelineLarge .OxMarkerPointIn { + position: absolute; + top: 64px; + width: 6px; + height: 6px; + margin-left: -5px; + z-index: 10; +} +.OxTimelineLarge .OxMarkerPointOut { + position: absolute; + top: 64px; + width: 6px; + height: 6px; + z-index: 10; +} +.OxTimelineLarge .OxMarkerPosition { + position: absolute; + top: 2px; + width: 9px; + height: 5px; + z-index: 10; +} +.OxTimelineLarge .OxSubtitle { + position: absolute; + bottom: 9px; + border: 1px solid rgba(255, 255, 255, 0.5); + padding: 1px; + background: rgba(0, 0, 0, 0.25); + font-size: 8px; + line-height: 10px; + text-align: center; + text-overflow: ellipsis; + text-shadow: rgba(0, 0, 0, 1) 1px 1px 1px; + color: rgb(255, 255, 255); + cursor: default; + overflow: hidden; + z-index: 10; + -moz-box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); +} +.OxTimelineLarge .OxSubtitle.OxHighlight { + border-color: rgba(255, 255, 0, 1); +} +.OxTimelineSmall { + position: absolute; +} +.OxTimelineSmall > div { + position: absolute; + height: 18px; + margin: 3px 4px 3px 4px; + overflow: hidden; +} +.OxTimelineSmall > div > img { + position: absolute; + left: 0; + top: 0; +} +.OxTimelineSmall > div > .OxTimelineSmallImage { + margin-top: 1px; +} +.OxTimelineSmall .OxMarkerPointIn { + position: absolute; + width: 6px; + height: 6px; + margin-left: -1px; +} +.OxTimelineSmall .OxMarkerPointOut { + position: absolute; + width: 6px; + height: 6px; + margin-left: 4px; +} + +.OxVideoPlayer > .OxBar .OxInputGroup { + //width: 98px; +} +.OxVideoPlayer > .OxBar .OxButton { + margin-right: -1px; +} +.OxVideoPlayer > .OxBar .OxButton, +.OxVideoPlayer > .OxBar .OxInput, +.OxVideoPlayer > .OxBar .OxLabel { + padding: 0; + -moz-border-radius: 0; + -webkit-border-radius: 0; +} +.OxVideoPlayer > .OxBar .OxLabel { + //width: 22px; + //background: rgb(32, 32, 32); +} +.OxVideoPlayer .OxMarkerFrame { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 16px; +} +.OxVideoPlayer .OxMarkerFrame > div { + float: left; +} +.OxVideoPlayer .OxMarkerFrame > .OxFrame { + background: rgba(0, 0, 0, 0.5); +} +.OxVideoPlayer .OxMarkerFrame > .OxPoster { + border: 1px solid rgba(255, 255, 255, 0.5); +} +.OxVideoPlayer .OxMarkerPoint { + position: absolute; + width: 16px; + height: 16px; +} +.OxVideoPlayer .OxMarkerInTop { + left: 4px; + top: 4px; +} +.OxVideoPlayer .OxMarkerInBottom { + left: 4px; + bottom: 20px; +} +.OxVideoPlayer .OxMarkerOutTop { + right: 4px; + top: 4px; +} +.OxVideoPlayer .OxMarkerOutBottom { + right: 4px; + bottom: 20px; +} +.OxVideoPlayer .OxSubtitle { + position: absolute; + left: 0; + right: 0; + text-align: center; + //text-shadow: rgba(0, 0, 0, 1) 2px 2px 0px; + text-shadow: rgba(0, 0, 0, 1) 0 0 4px; + color: rgb(255, 255, 255); + z-index: 10; +} \ No newline at end of file diff --git a/pandora/static/js/timeline.js b/pandora/static/js/timeline.js new file mode 100644 index 000000000..9ae16b37e --- /dev/null +++ b/pandora/static/js/timeline.js @@ -0,0 +1,1299 @@ +$(function() { + + /* + h1 is 0.5-12 times as large as h0 + w1 is 0.5-12*ratio as large as w0 + h1 = (h0 - 24) / 2 + w1 = h1 * ratio + w0 = pageWidth - 8 - w1 + */ + var $body = $("body"), + $document = $(document), + $window = $(window), + $editor, + $panel, + $player = [], + $players, + $timelines, + $timelineLarge, + $timelineSmall, + find = "cinema", + matches = [3, 22, 24, 55, 57, 61, 70, 105, 118, 122, 150, 152, 155], + pageWidth = $document.width() - 384 - 2, + posterFrame = 1515, + points = [2059, 2748], + videoId = document.location.hash.substring(1); + + $.getJSON("/" + videoId + "/data/video.json", function(video) { + var duration = video.duration, + videoRatio = video.aspectRatio, + videoHeight = 96, + videoWidth = parseInt(video.aspectRatio*videoHeight), + position = duration/2; + + videoWidth += videoWidth%2; + //resizeVideoPlayers(pageWidth); + + Ox.Editor = function(options, self) { + + var self = self || {}, + that = new Ox.Element("div", self) + .defaults({ + cuts: [], + duration: 0, + find: "", + largeTimeline: true, + matches: [], + points: [0, 0], + position: 0, + posterFrame: 0, + subtitles: [], + videoHeight: 0, + videoId: "", + videoWidth: 0, + videoSize: "large", + width: 0 + }) + .options(options || {}) + .addClass("OxEditor"); + + $.extend(self, { + $player: [], + $timeline: [], + controlsHeight: 16, + margin: 8, + videoRatio: self.options.videoWidth / self.options.videoHeight + }); + self.sizes = getSizes(); + + $.each(["play", "in", "out"], function(i, type) { + self.$player[i] = new Ox.VideoPlayer({ + duration: self.options.duration, + find: self.options.find, + height: self.sizes.player[i].height, + id: "player" + Ox.toTitleCase(type), + points: self.options.points, + position: type == "play" ? self.options.position : self.options.points[type == "in" ? 0 : 1], + posterFrame: self.options.posterFrame, + subtitles: self.options.subtitles, + type: type, + url: "/" + videoId + "/96p." + ($.support.video.webm ? "webm": "mp4"), + width: self.sizes.player[i].width + }) + .css({ + left: self.sizes.player[i].left + "px", + top: self.sizes.player[i].top + "px" + }) + .appendTo(that); + if (type == "in" || type == "out") { + self.$player[i].bindEvent({ + change: function() { + goToPoint(type); + }, + set: function() { + setPoint(type); + } + }) + } + }); + self.$player[0].bindEvent("change", changePlayer); + + self.$timeline[0] = new Ox.LargeTimeline({ + cuts: self.options.cuts, + duration: self.options.duration, + find: self.options.find, + id: "timelineLarge", + matches: self.options.matches, + points: self.options.points, + position: self.options.position, + subtitles: self.options.subtitles, + videoId: self.options.videoId, + width: self.sizes.timeline[0].width + }) + .css({ + left: self.sizes.timeline[0].left + "px", + top: self.sizes.timeline[0].top + "px" + }) + .bindEvent("change", changeTimelineLarge) + .bindEvent("changeEnd", changeTimelineLarge) + .appendTo(that); + + self.$timeline[1] = new Ox.SmallTimeline({ + cuts: self.options.cuts, + duration: self.options.duration, + find: self.options.find, + id: "timelineSmall", + matches: self.options.matches, + points: self.options.points, + position: self.options.position, + subtitles: self.options.subtitles, + videoId: self.options.videoId, + width: self.sizes.timeline[1].width + }) + .css({ + left: self.sizes.timeline[1].left + "px", + top: self.sizes.timeline[1].top + "px" + }) + .bindEvent("change", changeTimelineSmall) + .appendTo(that); + + that.addEvent({ + key_alt_left: function() { + movePositionBy(-0.04); + }, + key_alt_right: function() { + movePositionBy(0.04); + }, + key_alt_shift_left: function() { + movePositionTo("cut", -1); + }, + key_alt_shift_right: function() { + movePositionTo("cut", 1); + }, + key_closebracket: function() { + goToPoint("out"); + }, + key_comma: function() { + movePositionTo("subtitle", -1); + }, + key_dot: function() { + movePositionTo("subtitle", 1); + }, + key_down: function() { + movePositionBy(self.options.width - 2 * self.margin); + }, + key_i: function() { + setPoint("in"); + }, + key_left: function() { + movePositionBy(-1); + }, + key_m: toggleMute, + key_o: function() { + setPoint("out"); + }, + key_openbracket: function() { + goToPoint("in"); + }, + key_p: playInToOut, + key_right: function() { + movePositionBy(1); + }, + key_shift_comma: function() { + movePositionTo("match", -1) + }, + key_shift_dot: function() { + movePositionTo("match", 1) + }, + key_shift_down: function() { + movePositionBy(self.options.duration); + }, + key_shift_left: function() { + movePositionBy(-60); + }, + key_shift_right: function() { + movePositionBy(60); + }, + key_shift_up: function() { + movePositionBy(-self.options.duration); + }, + key_space: togglePlay, + key_up: function() { + movePositionBy(-(self.options.width - 2 * self.margin)); + } + }); + + that.gainFocus(); + + function changePlayer(event, data) { + self.options.position = data.position; + self.$timeline[0].options({ + position: data.position + }); + self.$timeline[1].options({ + position: data.position + }); + } + + function changeTimelineLarge(event, data) { + self.options.position = data.position; + self.$player[0].options({ + position: data.position + }); + self.$timeline[1].options({ + position: data.position + }); + } + + function changeTimelineSmall(event, data) { + self.options.position = data.position; + self.$player[0].options({ + position: data.position + }); + self.$timeline[0].options({ + position: data.position + }); + } + + function getNextPosition(type, direction) { + var found = false, + position = 0, + positions; + if (type == "cut") { + positions = self.options.cuts; + } else if (type == "match") { + positions = $.map(self.options.matches, function(v, i) { + return self.options.subtitles[v]["in"]; + }); + } else if (type == "subtitle") { + positions = $.map(self.options.subtitles, function(v, i) { + return v["in"]; + }); + } + direction == -1 && positions.reverse(); + $.each(positions, function(i, v) { + if (direction == 1 ? v > self.options.position : v < self.options.position) { + position = v; + found = true; + return false; + } + }); + direction == -1 && positions.reverse(); + if (!found) { + position = positions[direction == 1 ? 0 : positions.length - 1]; + } + return position; + } + + function getSizes() { + var size = { + player: [], + timeline: [] + }; + size.player[0] = { + left: self.margin / 2, + top: self.margin / 2, + width: Math.round((self.options.width - 3 * self.margin + (self.controlsHeight + self.margin) / 2 * self.videoRatio) * 2/3), + } + size.player[0].height = Math.round(size.player[0].width / self.videoRatio); + size.player[1] = { + left: size.player[0].left + size.player[0].width + self.margin, + top: size.player[0].top, + width: self.options.width - 3 * self.margin - size.player[0].width + } + size.player[1].height = Math.ceil(size.player[1].width / self.videoRatio) + size.player[2] = { + left: size.player[1].left, + top: size.player[0].top + size.player[1].height + self.controlsHeight + self.margin, + width: size.player[1].width, + height: size.player[0].height - size.player[1].height - self.controlsHeight - self.margin + } + size.timeline[0] = { + left: self.margin / 2, + top: size.player[0].height + self.controlsHeight + 1.5 * self.margin, + width: self.options.width - 2 * self.margin, + height: 64 + } + size.timeline[1] = { + left: size.timeline[0].left, + top: size.timeline[0].top + size.timeline[0].height + self.margin, + width: size.timeline[0].width + } + return size; + } + + function goToPoint(point) { + self.options.position = self.options.points[point == "in" ? 0 : 1]; + setPosition(); + that.triggerEvent("change", { + position: self.options.position + }); + } + + function movePositionBy(sec) { + self.options.position = Ox.limit(self.options.position + sec, 0, self.options.duration); + setPosition(); + that.triggerEvent("change", { + position: self.options.position + }); + } + + function movePositionTo(type, direction) { + self.options.position = getNextPosition(type, direction); + setPosition(); + that.triggerEvent("change", { + position: self.options.position + }); + } + + function playInToOut() { + self.$player[0].playInToOut(); + } + + function resizeEditor(event, data) { + var width = data - 2 * margin + 100; + resizeVideoPlayers(width); + $timelineLarge.options({ + width: width + }); + $timelineSmall.options({ + width: width + }); + } + + function resizePlayers() { + $.each(self.$player, function(i, v) { + v.options({ + width: size[i].width, + height: size[i].height + }) + .css({ + left: size[i].left + "px", + top: size[i].top + "px", + }); + }); + } + + function setPoint(point) { + self.options.points[point == "in" ? 0 : 1] = self.options.position; + self.$player[point == "in" ? 1 : 2].options({ + position: self.options.position + }); + if (self.options.points[1] < self.options.points[0]) { + self.options.points[point == "in" ? 1 : 0] = self.options.position; + self.$player[point == "in" ? 2 : 1].options({ + position: self.options.position + }); + } + $.each(self.$player, function(i, v) { + v.options({ + points: self.options.points + }); + }); + $.each(self.$timeline, function(i, v) { + v.options({ + points: self.options.points + }); + }); + } + + function setPosition() { + self.$player[0].options({ + position: self.options.position + }); + $.each(self.$timeline, function(i, v) { + v.options({ + position: self.options.position + }); + }); + } + + function toggleMute() { + self.$player[0].toggleMute(); + } + + function togglePlay() { + self.$player[0].togglePlay(); + } + + self.onChange = function(key, value) { + if (key == "width") { + self.sizes = getSizes(); + $.each(self.$player, function(i, v) { + v.options({ + height: self.sizes.player[i].height, + width: self.sizes.player[i].width + }) + .css({ + left: self.sizes.player[i].left + "px", + top: self.sizes.player[i].top + "px" + }); + }); + $.each(self.$timeline, function(i, v) { + v.options({ + width: self.sizes.timeline[i].width + }) + .css({ + left: self.sizes.timeline[i].left + "px", + top: self.sizes.timeline[i].top + "px" + }); + }); + } + }; + + return that; + + }; + + Ox.LargeTimeline = function(options, self) { + + var self = self || {}, + that = new Ox.Element("div", self) + .defaults({ + cuts: [], + duration: 0, + find: "", + matches: [], + points: [0, 0], + position: 0, + subtitles: [], + videoId: "", + width: 0 + }) + .options(options || {}) + .addClass("OxTimelineLarge") + .mousedown(mousedown) + .mouseleave(mouseleave) + .mousemove(mousemove); + + $.extend(self, { + $cuts: [], + $markerPoint: [], + $subtitles: [], + $tiles: {}, + center: parseInt(self.options.width / 2), + element: that.$element[0], + fps: 25, + height: 64, + tileWidth: 1500 + }); + self.tiles = self.options.duration * self.fps / self.tileWidth; + + self.$timeline = $("
") + .css({ + left: self.center + "px" + }) + .appendTo(that.$element) + + $.each(self.options.subtitles, function(i, v) { + self.$subtitles[i] = $("
") + .addClass("OxSubtitle" + (self.options.matches.indexOf(i) > -1 ? " OxHighlight" : "")) + .css({ + left: (v["in"] * self.fps) + "px", + width: (((v["out"] - v["in"]) * self.fps) - 4) + "px" + }) + .html(highlight(v.text, self.options.find)) + .appendTo(self.$timeline) + }); + + $.each(self.options.cuts, function(i, v) { + self.$cuts[i] = $("") + .addClass("OxCut") + .attr({ + src: "/static/oxjs/build/png/ox.ui/videoMarkerCut.png" + }) + .css({ + left: (v * self.fps) + "px" + }) + .appendTo(self.$timeline) + }); + + self.$markerPosition = $("") + .addClass("OxMarkerPosition") + .attr({ + src: "/static/oxjs/build/png/ox.ui/videoMarkerPlay.png" + }) + .appendTo(that.$element); + setMarker(); + + $.each(["In", "Out"], function(i, v) { + self.$markerPoint[i] = $("") + .addClass("OxMarkerPoint" + v) + .attr({ + src: "/static/oxjs/build/png/ox.ui/videoMarker" + v + ".png" + }) + .appendTo(self.$timeline); + setMarkerPoint(i); + }); + + setWidth(); + setPosition(); + + function mousedown(e) { + var mousemove = false, + x = e.clientX; + $window.mousemove(function(e) { + mousemove = true; + self.options.position = Ox.limit( + self.options.position + (x - e.clientX) / self.fps, + 0, self.options.duration + ); + x = e.clientX; + setPosition(); + that.triggerEvent("change", { + position: self.options.position + }); + }); + $window.one("mouseup", function() { + $window.unbind("mousemove"); + if (!mousemove) { + self.options.position = Ox.limit( + self.options.position + (e.clientX - that.$element.offset().left - self.center) / self.fps, + 0, self.options.duration + ); + setPosition(); + } + that.triggerEvent("change", { + position: self.options.position + }); + }); + e.preventDefault(); + } + + function mouseleave(e) { + self.$tooltip.hide(); + } + + function mousemove(e) { + if (!self.$tooltip) { + self.$tooltip = Ox.Tooltip(); + } + self.$tooltip + .options({ + title: Ox.formatDuration(self.options.position + (e.clientX - that.offset().left - self.center) / self.fps, 3) + }) + .show(e.clientX, e.clientY); + } + + function setMarkerPoint(i) { + self.$markerPoint[i].css({ + left: (self.options.points[i] * self.fps) + "px" + }); + } + + function setMarker() { + self.$markerPosition.css({ + left: (self.center - 4) + "px", + }); + } + + function setPosition() { + self.tile = parseInt(self.options.position * self.fps / self.tileWidth); + self.$timeline.css({ + marginLeft: (-self.options.position * self.fps) + "px" + }); + $.each(Ox.range(Math.max(self.tile - 1, 0), Math.min(self.tile + 2, self.tiles)), function(i, v) { + if (!self.$tiles[v]) { + self.$tiles[v] = $("") + .attr({ + src: "/" + self.options.videoId + "/timelines/" + (window.location.hash == "#strip" ? "strip" : "timeline") + ".64." + v + ".png" + }) + .css({ + left: (v * self.tileWidth) + "px" + }) + .appendTo(self.$timeline); + } + }); + } + + function setWidth() { + self.center = parseInt(self.options.width / 2); + that.css({ + width: self.options.width + "px" + }); + self.$timeline.css({ + left: self.center + "px" + }); + setMarker(); + } + + self.onChange = function(key, value) { + if (key == "points") { + setMarkerPoint(0); + setMarkerPoint(1); + } else if (key == "position") { + setPosition(); + } else if (key == "width") { + setWidth(); + } + }; + + return that; + + }; + + Ox.SmallTimeline = function(options, self) { + + var self = self || {}, + that = new Ox.Element("div", self) + .defaults({ + cuts: [], + duration: 0, + find: "", + matches: [], + points: [0, 0], + position: 0, + subtitles: [], + videoId: "", + width: 0 + }) + .options(options || {}) + .addClass("OxTimelineSmall") + .mousedown(mousedown) + .mouseleave(mouseleave) + .mousemove(mousemove); + + $.extend(self, { + $images: [], + $lines: [], + $markerPoint: [], + $subtitles: [], + height: 16, + lines: Math.ceil(self.options.duration / self.options.width), + margin: 8, + subtitlesImageURL: getSubtitlesImageURL() + }); + + $.each(Ox.range(0, self.lines), function(i) { + addLine(i); + }); + + self.$markerPosition = $("") + .addClass("OxMarkerPosition") + .attr({ + src: "/static/oxjs/build/png/ox.ui/videoMarkerPlay.png" + }) + .css({ + position: "absolute", + width: "9px", + height: "5px", + zIndex: 10 + }) + .appendTo(that.$element); + + setPosition(); + + $.each(["in", "out"], function(i, v) { + var titleCase = Ox.toTitleCase(v); + self.$markerPoint[i] = $("") + .addClass("OxMarkerPoint" + titleCase) + .attr({ + src: "/static/oxjs/build/png/ox.ui/videoMarker" + titleCase + ".png" + }) + .appendTo(that.$element); + setMarkerPoint(i); + }); + + function addLine(i) { + self.$lines[i] = new Ox.Element("div") + .css({ + top: i * (self.height + self.margin) + "px", + width: self.options.width + "px" + }) + .appendTo(that); + self.$images[i] = $("") + .addClass("OxTimelineSmallImage") + .attr({ + src: "/" + self.options.videoId + "/timelines/timeline.16." + 0 + ".png" + }) + .css({ + marginLeft: (-i * self.options.width) + "px" + }) + .appendTo(self.$lines[i].$element); + self.$subtitles[i] = $("") + .attr({ + src: self.subtitlesImageURL + }) + .css({ + marginLeft: (-i * self.options.width) + "px" + }) + .appendTo(self.$lines[i].$element); + } + + function getSubtitle(position) { + var subtitle = null; + $.each(self.options.subtitles, function(i, v) { + if (v["in"] <= position && v["out"] >= position) { + subtitle = v; + return false; + } + }); + return subtitle; + } + + function getSubtitlesImageURL() { + var height = 18, + width = Math.ceil(self.options.duration), + $canvas = $("") + .attr({ + height: height, + width: width + }), + canvas = $canvas[0], + context = canvas.getContext("2d"), + imageData = context.createImageData(width, height), + data = imageData.data; + $.each(self.options.subtitles, function(i, v) { + var color = matches.indexOf(i) > -1 ? [255, 255, 0] : [255, 255, 255] + $.each(Ox.range( + Math.round(v["in"]), + Math.round(v["out"]) + 1 + ), function(i, x) { + $.each(Ox.range(0, 18), function(i, y) { + var index = x * 4 + y * 4 * width; + data[index] = color[0]; + data[index + 1] = color[1]; + data[index + 2] = color[2]; + data[index + 3] = (y == 0 || y == 17) ? 255 : 128 + }); + }); + }); + context.putImageData(imageData, 0, 0); + return canvas.toDataURL() + } + + function mousedown(e) { + var offset = that.offset(), + x = e.clientX, + y = e.clientY + //FIXME: this might still be broken in opera according to http://acko.net/blog/mouse-handling-and-absolute-positions-in-javascript + self.options.position = e.offsetX?e.offsetX:e.clientX-$(e.target).offset().left; + setPosition(); + that.triggerEvent("change", { + position: self.options.position + }); + $window.mousemove(function(e) { + if ($(e.target).is("img")) { + self.options.position = e.offsetX?e.offsetX:e.clientX-$(e.target).offset().left; + setPosition(); + that.triggerEvent("change", { + position: self.options.position + }); + } + }); + $window.one("mouseup", function() { + $window.unbind("mousemove"); + }) + e.preventDefault(); + } + + function mouseleave(e) { + self.$tooltip.hide(); + } + + function mousemove(e) { + //FIXME: this might still be broken in opera according to http://acko.net/blog/mouse-handling-and-absolute-positions-in-javascript + var position = e.offsetX?e.offsetX:e.clientX-$(e.target).offset().left, + subtitle = getSubtitle(position); + Ox.print("position", position, e) + self.$tooltip = new Ox.Tooltip({ + title: subtitle ? + "" + + highlight(subtitle.text, self.options.find).replace(/\n/g, "
") + "

" + + Ox.formatDuration(subtitle["in"], 3) + " - " + Ox.formatDuration(subtitle["out"], 3) : + Ox.formatDuration(position) + }) + .css({ + textAlign: "center" + }) + .show(e.clientX, e.clientY); + } + + function setMarker() { + self.$markerPosition + .css({ + left: (self.options.position % self.options.width) + "px", + top: (parseInt(self.options.position / self.options.width) * (self.height + self.margin) + 2) + "px", + }); + } + + function setMarkerPoint(i) { + var position = self.options.points[i]; + self.$markerPoint[i] + .css({ + left: (position % self.options.width) + "px", + top: (parseInt(position / self.options.width) * (self.height + self.margin) + 16) + "px", + }); + } + + function setPosition() { + self.options.position = Ox.limit(self.options.position, 0, self.options.duration); + setMarker(); + } + + function setWidth() { + self.lines = Math.ceil(self.options.duration / self.options.width); + $.each(Ox.range(self.lines), function(i, v) { + if (self.$lines[i]) { + self.$lines[i].css({ + width: self.options.width + "px" + }); + self.$images[i].css({ + marginLeft: (-i * self.options.width) + "px" + }); + self.$subtitles[i].css({ + marginLeft: (-i * self.options.width) + "px" + }); + } else { + addLine(i); + } + }); + while (self.$lines.length > self.lines) { + self.$lines[self.$lines.length - 1].remove(); + self.$lines.pop(); + } + setMarker(); + setMarkerPoint(0); + setMarkerPoint(1); + } + + self.onChange = function(key, value) { + if (key == "points") { + setMarkerPoint(0); + setMarkerPoint(1); + } else if (key == "position") { + setPosition(); + } else if (key == "width") { + setWidth(); + } + }; + + return that; + + }; + + Ox.VideoPlayer = function(options, self) { + + var self = self || {}, + that = new Ox.Element("div", self) + .defaults({ + find: "", + height: 0, + points: [0, 0], + position: 0, + posterFrame: 0, + subtitles: [], + type: "play", + url: "", + width: 0 + }) + .options(options || {}) + .addClass("OxVideoPlayer") + .css({ + height: (self.options.height + 16) + "px", + width: self.options.width + "px" + }); + + self.controlsHeight = 16; + + self.$video = $("