stream, videosupport

This commit is contained in:
j 2010-09-03 15:28:44 +02:00
parent fba5070bc2
commit 052aeb7ba4
8 changed files with 203 additions and 208 deletions

View file

@ -34,6 +34,10 @@ def parse_decimal(string):
d = string.split('/') d = string.split('/')
return Decimal(d[0]) / Decimal(d[1]) return Decimal(d[0]) / Decimal(d[1])
def file_path(f, name):
h = f.oshash
return os.path.join('file', h[:2], h[2:4], h[4:6], h[6:], name)
class File(models.Model): class File(models.Model):
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True) modified = models.DateTimeField(auto_now=True)
@ -136,11 +140,32 @@ class File(models.Model):
r[k] = unicode(self[k]) r[k] = unicode(self[k])
return r return r
#upload and data handling
video_available = models.BooleanField(default=False)
video = models.FileField(null=True, blank=True, upload_to=lambda f, x: file_path(f, '%s.webm'%settings.VIDEO_PROFILE))
data = models.FileField(null=True, blank=True, upload_to=lambda f, x: file_path(f, 'data.raw'))
def contents(self): def contents(self):
if self.contents_set.count() > 0: if self.data:
return self.contents_set.all()[0].data return self.data.read()
return None return None
def editable(self, user):
#FIXME: check that user has instance of this file
return True
def save_chunk(self, chunk, chunk_id=-1):
if not self.video_available:
if not self.video:
self.video.save('%s.webm'%settings.VIDEO_PROFILE, chunk)
else:
f = open(self.video.path, 'a')
#FIXME: should check that chunk_id/offset is right
f.write(chunk.read())
f.close()
return True
return False
class Volume(models.Model): class Volume(models.Model):
class Meta: class Meta:
unique_together = ("user", "name") unique_together = ("user", "name")
@ -198,88 +223,4 @@ class Frame(models.Model):
def __unicode__(self): def __unicode__(self):
return u'%s at %s' % (self.file, self.position) return u'%s at %s' % (self.file, self.position)
def stream_path(f):
h = f.file.oshash
return os.path.join('stream', h[:2], h[2:4], h[4:6], h[6:], f.profile)
class Stream(models.Model):
class Meta:
unique_together = ("file", "profile")
file = models.ForeignKey(File, related_name='streams')
profile = models.CharField(max_length=255, default='96p.webm')
video = models.FileField(default=None, blank=True, upload_to=lambda f, x: stream_path(f))
source = models.ForeignKey('Stream', related_name='derivatives', default=None, null=True)
available = models.BooleanField(default=False)
def __unicode__(self):
return self.video
def extract_derivates(self):
if settings.VIDEO_H264:
profile = self.profile.replace('.webm', '.mp4')
if Stream.objects.filter(profile=profile, source=self).count() == 0:
derivate = Stream(file=self.file, source=self, profile=profile)
derivate.video.name = self.video.name.replace(self.profile, profile)
derivate.encode()
for p in settings.VIDEO_DERIVATIVES:
profile = p + '.webm'
target = self.video.path.replace(self.profile, profile)
if Stream.objects.filter(profile=profile, source=self).count() == 0:
derivate = Stream(file=self.file, source=self, profile=profile)
derivate.video.name = self.video.name.replace(self.profile, profile)
derivate.encode()
if settings.VIDEO_H264:
profile = p + '.mp4'
if Stream.objects.filter(profile=profile, source=self).count() == 0:
derivate = Stream(file=self.file, source=self, profile=profile)
derivate.video.name = self.video.name.replace(self.profile, profile)
derivate.encode()
return True
def encode(self):
if self.source:
video = self.source.video.path
target = self.video.path
profile = self.profile
info = self.file.info
if extract.stream(video, target, profile, info):
self.available=True
self.save()
def editable(self, user):
#FIXME: possibly needs user setting for stream
return True
def save_chunk(self, chunk, chunk_id=-1):
if not self.available:
if not self.video:
self.video.save(self.profile, chunk)
else:
f = open(self.video.path, 'a')
#FIXME: should check that chunk_id/offset is right
f.write(chunk.read())
f.close()
return True
return False
def save(self, *args, **kwargs):
if self.available and not self.file.available:
self.file.available = True
self.file.save()
super(Stream, self).save(*args, **kwargs)
class FileContents(models.Model):
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
file = models.ForeignKey(File, related_name="contents_set")
data = models.TextField(default=u'')
def save(self, *args, **kwargs):
if self.data and not self.file.available:
self.file.available = True
self.file.save()
super(FileContents, self).save(*args, **kwargs)

View file

@ -183,10 +183,8 @@ def api_upload(request):
else: else:
response = json_response(status=403, text='permissino denied') response = json_response(status=403, text='permissino denied')
if 'file' in request.FILES: if 'file' in request.FILES:
if f.contents.count() == 0: if not f.data:
contents = models.FileContents(file=f) f.data.save('data.raw', request.FILES['file'])
contents.data = request.FILES['file'].read()
contents.save()
response = json_response({}) response = json_response({})
else: else:
response = json_response(status=403, text='permissino denied') response = json_response(status=403, text='permissino denied')
@ -203,44 +201,42 @@ def firefogg_upload(request):
oshash = request.GET['oshash'] oshash = request.GET['oshash']
#handle video upload #handle video upload
if request.method == 'POST': if request.method == 'POST':
#init upload
#post next chunk #post next chunk
if 'chunk' in request.FILES and oshash: if 'chunk' in request.FILES and oshash:
stream = get_object_or_404(models.Stream, file__oshash=oshash, profile=profile) f = get_object_or_404(models.File, oshash=oshash)
form = VideoChunkForm(request.POST, request.FILES) form = VideoChunkForm(request.POST, request.FILES)
if form.is_valid() and stream.editable(request.user): if form.is_valid() and profile == settings.VIDEO_PROFILE and f.editable(request.user):
c = form.cleaned_data['chunk'] c = form.cleaned_data['chunk']
chunk_id = form.cleaned_data['chunkId'] chunk_id = form.cleaned_data['chunkId']
response = { response = {
'result': 1, 'result': 1,
'resultUrl': request.build_absolute_uri('/') 'resultUrl': request.build_absolute_uri('/')
} }
if not stream.save_chunk(c, chunk_id): if not f.save_chunk(c, chunk_id):
response['result'] = -1 response['result'] = -1
elif form.cleaned_data['done']: elif form.cleaned_data['done']:
#FIXME: send message to encode deamon to create derivates instead #FIXME: send message to encode deamon to create derivates instead
stream.available = True f.available = True
stream.save() f.save()
response['result'] = 1 response['result'] = 1
response['done'] = 1 response['done'] = 1
return render_to_json_response(response) return render_to_json_response(response)
#FIXME: check for valid profile #init upload
elif oshash: elif oshash and profile == settings.VIDEO_PROFILE:
#404 if oshash is not know, files must be registered via update api first #404 if oshash is not know, files must be registered via update api first
f = get_object_or_404(models.File, oshash=oshash) f = get_object_or_404(models.File, oshash=oshash)
stream, created = models.Stream.objects.get_or_create(file=f, profile=profile) if f.editable(request.user):
if stream.video: #FIXME: check permission here instead of just starting over if f.video:
stream.video.delete() f.video.delete()
stream.available = False f.video_available = False
stream.save() f.save()
response = { response = {
#is it possible to no hardcode url here? #is it possible to no hardcode url here?
'uploadUrl': request.build_absolute_uri('/api/upload/?oshash=%s&profile=%s' % (f.oshash, profile)), 'uploadUrl': request.build_absolute_uri('/api/upload/?oshash=%s&profile=%s' % (f.oshash, profile)),
'result': 1 'result': 1
} }
return render_to_json_response(response) return render_to_json_response(response)
response = json_response(status=400, text='this request requires POST') response = json_response(status=400, text='this request requires POST')
return render_to_json_response(response) return render_to_json_response(response)

View file

@ -1,77 +0,0 @@
# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
# GPL 2010
from __future__ import division
import re
import os
from os.path import abspath, join, dirname, exists
import shutil
import time
import warnings
import subprocess
import ox
import Image
import simplejson as json
img_extension='jpg'
def run_command(cmd, timeout=10):
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
while timeout > 0:
time.sleep(0.2)
timeout -= 0.2
if p.poll() != None:
return p.returncode
if p.poll() == None:
os.kill(p.pid, 9)
killedpid, stat = os.waitpid(p.pid, os.WNOHANG)
return p.returncode
def frame(videoFile, position, baseFolder, width=128, redo=False):
'''
params:
videoFile
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):
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]
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):
if exists(image_source):
source = Image.open(image_source)
source_width = source.size[0]
source_height = source.size[1]
height = int(width / (float(source_width) / source_height))
height = height - height % 2
if width < source_width:
resize_method = Image.ANTIALIAS
else:
resize_method = Image.BICUBIC
output = source.resize((width, height), resize_method)
output.save(image_output)

View file

@ -4,8 +4,10 @@ from __future__ import division, with_statement
from datetime import datetime from datetime import datetime
import os.path import os.path
import math
import random import random
import re import re
import subprocess
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
@ -23,7 +25,7 @@ from firefogg import Firefogg
import managers import managers
import load import load
import utils import utils
import extract from archive import extract
def getMovie(info): def getMovie(info):
@ -204,14 +206,28 @@ class Movie(models.Model):
movie[pub_key] = value() movie[pub_key] = value()
else: else:
movie[pub_key] = value movie[pub_key] = value
movie['poster'] = self.get_poster() if not fields:
movie['poster'] = self.get_poster()
movie['stream'] = self.get_stream()
if fields: if fields:
for f in fields: for f in fields:
if f.endswith('.length') and f[:-7] in ('cast', 'genre', 'trivia'): if f.endswith('.length') and f[:-7] in ('cast', 'genre', 'trivia'):
movie[f] = getattr(self.sort, f[:-7]) movie[f] = getattr(self.sort, f[:-7])
return movie return movie
def get_stream(self):
stream = {}
if self.streams.all().count():
s = self.streams.all()[0]
if s.video and s.info:
stream['duration'] = s.info['duration']
if 'video' in s.info and s.info['video']:
stream['aspectRatio'] = s.info['video'][0]['width'] / s.info['video'][0]['height']
stream['baseUrl'] = os.path.dirname(s.video.url)
stream['profiles'] = list(set(map(lambda s: int(os.path.splitext(s['profile'])[0][:-1]), self.streams.all().values('profile'))))
return stream
def fields(self): def fields(self):
fields = {} fields = {}
for f in self._meta.fields: for f in self._meta.fields:
@ -227,13 +243,6 @@ class Movie(models.Model):
self.get('series title', ''), self.get('episode title', ''), self.get('series title', ''), self.get('episode title', ''),
self.get('season', ''), self.get('episode', '')) self.get('season', ''), self.get('episode', ''))
def streams(self):
streams = []
for f in self.files.filter(is_main=True, available=True):
for s in f.streams.all():
streams.append(s.video.url)
return streams
def frame(self, position, width=128): def frame(self, position, width=128):
#FIXME: compute offset and so on #FIXME: compute offset and so on
f = self.files.all()[0] f = self.files.all()[0]
@ -292,15 +301,7 @@ class Movie(models.Model):
#title #title
title = canonicalTitle(self.get('title')) title = canonicalTitle(self.get('title'))
title = re.sub(u'[\'!¿¡,\.;\-"\:\*\[\]]', '', title) s.title = utils.sort_title(title)
title = title.replace(u'Æ', 'Ae')
#pad numbered titles
numbers = re.compile('^(\d+)').findall(title)
if numbers:
padded = '%010d' % int(numbers[0])
title = re.sub('^(\d+)', padded, title)
s.title = title.strip()
s.country = ','.join(self.get('countries', [])) s.country = ','.join(self.get('countries', []))
s.year = self.get('year', '') s.year = self.get('year', '')
@ -373,6 +374,55 @@ class Movie(models.Model):
else: else:
Facet.objects.filter(movie=self, key='year').delete() Facet.objects.filter(movie=self, key='year').delete()
def updatePoster(self):
n = self.files.count() * 3
frame = int(math.floor(n/2))
part = 1
for f in self.files.filter(is_main=True, video_available=True):
for frame in f.frames.all():
path = os.path.abspath(os.path.join(settings.MEDIA_ROOT, poster_path(self)))
path = path.replace('.jpg', '%s.%s.jpg'%(part, frame.pos))
cmd = ['oxposter',
'-t', self.get('title'),
'-d', self.get('director'),
'-f', frame.frame.path,
'-p', path
]
if len(self.movieId) == 7:
cmd += ['-i', self.movieId]
else:
cmd += ['-o', self.movieId]
print cmd
subprocess.Popen(cmd)
part += 1
def updateStreams(self):
files = {}
for f in self.files.filter(is_main=True, video_available=True):
files[utils.sort_title(f.name)] = f.video.path
if files:
stream, created = Stream.objects.get_or_create(movie=self, profile='%s.webm' % settings.VIDEO_PROFILE)
stream.video.name = stream_path(stream)
cmd = []
for f in sorted(files):
if not cmd:
cmd.append(files[f])
else:
cmd.append('+')
cmd.append(files[f])
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'))
stream.extract_derivatives()
#something with poster
self.available = True
self.save()
class MovieFind(models.Model): class MovieFind(models.Model):
""" """
used to search movies, all search values are in here used to search movies, all search values are in here
@ -638,3 +688,61 @@ class Collection(models.Model):
def editable(self, user): def editable(self, user):
return self.users.filter(id=user.id).count() > 0 return self.users.filter(id=user.id).count() > 0
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)
class Stream(models.Model):
class Meta:
unique_together = ("movie", "profile")
movie = models.ForeignKey(Movie, related_name='streams')
profile = models.CharField(max_length=255, default='96p.webm')
video = models.FileField(default=None, blank=True, upload_to=lambda f, x: stream_path(f))
source = models.ForeignKey('Stream', related_name='derivatives', default=None, null=True)
available = models.BooleanField(default=False)
info = fields.DictField(default={})
#def __unicode__(self):
# return self.video
def extract_derivatives(self):
if settings.VIDEO_H264:
profile = self.profile.replace('.webm', '.mp4')
if Stream.objects.filter(profile=profile, source=self).count() == 0:
derivative = Stream(movie=self.movie, source=self, profile=profile)
derivative.video.name = self.video.name.replace(self.profile, profile)
derivative.encode()
for p in settings.VIDEO_DERIVATIVES:
profile = p + '.webm'
target = self.video.path.replace(self.profile, profile)
if Stream.objects.filter(profile=profile, source=self).count() == 0:
derivative = Stream(movie=movie.file, source=self, profile=profile)
derivative.video.name = self.video.name.replace(self.profile, profile)
derivative.encode()
if settings.VIDEO_H264:
profile = p + '.mp4'
if Stream.objects.filter(profile=profile, source=self).count() == 0:
derivative = Stream(movie=self.movie, source=self, profile=profile)
derivative.video.name = self.video.name.replace(self.profile, profile)
derivative.encode()
return True
def encode(self):
if self.source:
video = self.source.video.path
target = self.video.path
profile = self.profile
info = ox.avinfo(video)
if extract.stream(video, target, profile, info):
self.available=True
self.save()
def save(self, *args, **kwargs):
if self.video and not self.info:
self.info = ox.avinfo(self.video.path)
super(Stream, self).save(*args, **kwargs)

View file

@ -10,7 +10,7 @@ import hashlib
import ox import ox
import ox.iso import ox.iso
from ox.normalize import normalizeName, normalizeTitle from ox.normalize import normalizeName, normalizeTitle, canonicalTitle
def plural_key(term): def plural_key(term):
@ -149,3 +149,11 @@ def parse_path(path):
season=r['season'], episode=r['episode']) season=r['season'], episode=r['episode'])
return r return r
def sort_title(title):
#title
title = re.sub(u'[\'!¿¡,\.;\-"\:\*\[\]]', '', title)
title = title.replace(u'Æ', 'Ae')
#pad numbered titles
title = re.sub('(\d+)', lambda x: '%010d' % int(x.group(0)), title)
return title.strip()

View file

@ -69,7 +69,7 @@ TESTS_ROOT = join(PROJECT_ROOT, 'tests')
# URL that handles the media served from MEDIA_ROOT. Make sure to use a # URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases). # trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/" # Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = '' MEDIA_URL = '/media/'
STATIC_URL = '/static/' STATIC_URL = '/static/'

View file

@ -0,0 +1,18 @@
jQuery.support.video = function() {
jQuery.browser.chrome = /chrome/.test(navigator.userAgent.toLowerCase());
var video = {};
var v = document.createElement('video');
if (v) {
video.support = true;
video.webm = !!(v.canPlayType && v.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''));
//Disable WebM on Safari/Perian, seeking does not work
if(video.webm && jQuery.browser.safari && !jQuery.browser.chrome)
video.webm = false;
video.h264 = !!(v.canPlayType && v.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''));
video.ogg = !!(v.canPlayType && v.canPlayType('video/ogg; codecs="theora, vorbis"').replace(/no/, ''));
} else {
video.support = false;
}
return video;
}();

View file

@ -6,6 +6,7 @@
<link rel="stylesheet" type="text/css" href="/static/oxjs/build/css/ox.ui.css"/> <link rel="stylesheet" type="text/css" href="/static/oxjs/build/css/ox.ui.css"/>
<script type="text/javascript" src="/static/oxjs/build/js/jquery-1.4.2.js"></script> <script type="text/javascript" src="/static/oxjs/build/js/jquery-1.4.2.js"></script>
<script type="text/javascript" src="/static/js/jquery/jquery.videosupport.js"></script>
<script type="text/javascript" src="/static/oxjs/build/js/ox.js"></script> <script type="text/javascript" src="/static/oxjs/build/js/ox.js"></script>
<script type="text/javascript" src="/static/oxjs/build/js/ox.ui.js"></script> <script type="text/javascript" src="/static/oxjs/build/js/ox.ui.js"></script>
<script type="text/javascript" src="/site.js"></script> <script type="text/javascript" src="/site.js"></script>