serve frames, timelines and videos, wire up timeline demo

This commit is contained in:
j 2010-09-08 13:56:58 +02:00
parent 744dfb101e
commit f3f40f6e2f
10 changed files with 1595 additions and 68 deletions

View file

@ -16,6 +16,10 @@ def index(request):
context = RequestContext(request, {'settings':settings}) context = RequestContext(request, {'settings':settings})
return render_to_response('index.html', context) 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): def api_getPage(request):
data = json.loads(request.POST['data']) data = json.loads(request.POST['data'])
name = data['page'] name = data['page']

View file

@ -194,34 +194,22 @@ def run_command(cmd, timeout=10):
killedpid, stat = os.waitpid(p.pid, os.WNOHANG) killedpid, stat = os.waitpid(p.pid, os.WNOHANG)
return p.returncode return p.returncode
def frame(videoFile, position, baseFolder, width=128, redo=False): def frame(videoFile, frame, position, width=128, redo=False):
''' '''
params: params:
videoFile videoFile input
frame output
position as float in seconds position as float in seconds
baseFolder to write frames to
width of frame width of frame
redo boolean to extract file even if it exists 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 exists(videoFile):
frameFolder = os.path.dirname(frame)
if redo or not exists(frame): if redo or not exists(frame):
if not exists(baseFolder): if not exists(frameFolder):
os.makedirs(baseFolder) os.makedirs(frameFolder)
cmd = ['oggThumb', '-t', str(position), '-n', frame, '-s', '%dx0'%base_size, videoFile] cmd = ['oxframe', '-i', videoFile, '-o', frame, '-p', str(position), '-x', str(width)]
run_command(cmd) 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): def resize_image(image_source, image_output, width=None, size=None):
if exists(image_source): if exists(image_source):

View file

@ -259,9 +259,11 @@ class Movie(models.Model):
self.get('season', ''), self.get('episode', '')) self.get('season', ''), self.get('episode', ''))
def frame(self, position, width=128): def frame(self, position, width=128):
#FIXME: compute offset and so on stream = self.streams.filter(profile=settings.VIDEO_PROFILE+'.webm')[0]
f = self.files.all()[0] path = os.path.join(settings.MEDIA_ROOT, 'frame', self.movieId, "%d"%width, "%s.jpg"%position)
return f.frame(position, width) if not os.path.exists(path):
extract.frame(stream.video.path, path, position, width)
return path
def updateFind(self): def updateFind(self):
try: try:
@ -411,6 +413,10 @@ class Movie(models.Model):
subprocess.Popen(cmd) subprocess.Popen(cmd)
part += 1 part += 1
@property
def timeline_prefix(self):
return os.path.join('stream', movieid_path(self.movieId), 'timeline')
def updateStreams(self): def updateStreams(self):
files = {} files = {}
for f in self.files.filter(is_main=True, video_available=True): for f in self.files.filter(is_main=True, video_available=True):
@ -428,11 +434,13 @@ class Movie(models.Model):
else: else:
cmd.append('+') cmd.append('+')
cmd.append(files[f]) 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 cmd = [ 'mkvmerge', '-o', stream.video.path ] + cmd
subprocess.Popen(cmd) subprocess.Popen(cmd)
stream.save() 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() stream.extract_derivatives()
#something with poster #something with poster
@ -705,9 +713,12 @@ 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 movieid_path(h):
return os.path.join(h[:2], h[2:4], h[4:6], h[6:])
def stream_path(f): def stream_path(f):
h = f.movie.movieId 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 Stream(models.Model):
class Meta: class Meta:

View file

@ -5,10 +5,13 @@ from django.conf.urls.defaults import *
urlpatterns = patterns("backend.views", urlpatterns = patterns("backend.views",
(r'^frame/(?P<id>.*)/(?P<position>.*)\.(?P<size>\d+).jpg$', 'frame'), (r'^(?P<id>.*)/frame/(?P<size>\d+)/(?P<position>[0-9\.,]+).jpg$', 'frame'),
(r'^stream/(?P<id>.*).(?P<quality>.*).ogv$', 'video'), (r'^(?P<id>.*)/(?P<profile>.*.webm)$', 'video'),
(r'^poster/(?P<id>.*)\.(?P<size>\d+)\.jpg$', 'poster'), (r'^(?P<id>.*)/(?P<profile>.*.mp4)$', 'video'),
(r'^poster/(?P<id>.*)\.jpg$', 'poster'), (r'^(?P<id>.*)/poster\.(?P<size>\d+)\.jpg$', 'poster'),
(r'^(?P<id>.*)/poster\.jpg$', 'poster'),
(r'^(?P<id>.*)/timelines/timeline\.(?P<size>\d+)\.(?P<position>\d+)\.png$', 'timeline'),
(r'^(?P<id>.*)/data/(?P<data>.+)\.json$', 'data'),
(r'^api/$', 'api'), (r'^api/$', 'api'),
) )

View file

@ -494,45 +494,6 @@ def api_getImdbId(request):
response = json_response(status=404, text='not found') response = json_response(status=404, text='not found')
return render_to_json_response(response) 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): def apidoc(request):
''' '''
this is used for online documentation at http://127.0.0.1:8000/api/ 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, context = RequestContext(request, {'api': api,
'sitename': settings.SITENAME,}) 'sitename': settings.SITENAME,})
return render_to_response('api.html', context) 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)

View file

@ -14,8 +14,21 @@ DEBUG = True
TEMPLATE_DEBUG = DEBUG TEMPLATE_DEBUG = DEBUG
JSON_DEBUG = True JSON_DEBUG = True
#with apache x-sendfile or lighttpd set this to True
XSENDFILE = False XSENDFILE = False
XACCELREDIRECT = False
# with nginx:
#XACCELREDIRECT=[/some/path/, /protected/]
'''
this assumes the following configuration:
location /protected/ {
internal;
root /some/path/;
}
'''
ADMINS = ( ADMINS = (
('j', 'j@mailb.org'), ('j', 'j@mailb.org'),
) )

View file

@ -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;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<html>
<head>
<title>timeline demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" type="text/css" href="/static/oxjs/build/css/ox.ui.css"/>
<link rel="stylesheet" type="text/css" href="/static/css/timeline.css"/>
<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.data.js"></script>
<script type="text/javascript" src="/static/oxjs/build/js/ox.ui.js"></script>
<script type="text/javascript" src="/static/js/timeline.js"></script>
</head>
<body></body>
</html>

View file

@ -15,6 +15,7 @@ urlpatterns = patterns('',
(r'^pandora.json$', 'app.views.pandora_json'), (r'^pandora.json$', 'app.views.pandora_json'),
(r'^$', 'app.views.intro'), (r'^$', 'app.views.intro'),
(r'^ra$', 'app.views.index'), (r'^ra$', 'app.views.index'),
(r'^timeline$', 'app.views.timeline'),
(r'^r/(?P<key>.*)$', 'oxuser.views.recover'), (r'^r/(?P<key>.*)$', 'oxuser.views.recover'),
(r'', include('backend.urls')), (r'', include('backend.urls')),