PK!video_encoding/__init__.pyPK! ?video_encoding/admin.pyfrom django.contrib.contenttypes import admin from .models import Format class FormatInline(admin.GenericTabularInline): model = Format fields = ('format', 'progress', 'file', 'width', 'height', 'duration') readonly_fields = fields extra = 0 max_num = 0 def has_add_permission(self, *args): return False def has_delete_permission(self, *args, **kwargs): return False PK![[#video_encoding/backends/__init__.pyfrom django.core.exceptions import ImproperlyConfigured from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ from ..config import settings def get_backend_class(): try: cls = import_string(settings.VIDEO_ENCODING_BACKEND) except ImportError as e: raise ImproperlyConfigured( _("Cannot retrieve backend '{}'. Error: '{}'.").format( settings.VIDEO_ENCODING_BACKEND, e)) return cls def get_backend(): cls = get_backend_class() return cls(**settings.VIDEO_ENCODING_BACKEND_PARAMS) PK!5=video_encoding/backends/base.pyimport abc import six class BaseEncodingBackend(six.with_metaclass(abc.ABCMeta)): # used as key to get all defined formats from `VIDEO_ENCODING_FORMATS` name = 'undefined' @classmethod def check(cls): return [] @abc.abstractmethod def encode(self, source_path, target_path, params): # pragma: no cover """ Encodes a video to a specified file. All encoder specific options are passed in using `params`. """ pass @abc.abstractmethod def get_media_info(self, video_path): # pragma: no cover """ Returns duration, width and height of the video as dict. """ pass @abc.abstractmethod def get_thumbnail(self, video_path): # pragma: no cover """ Extracts an image of a video and returns its path. If the requested thumbnail is not within the duration of the video an `InvalidTimeError` is thrown. """ pass PK!sb!video_encoding/backends/ffmpeg.pyimport json import locale import logging import os import re import tempfile from subprocess import PIPE, Popen import six from django.core import checks from .. import exceptions from ..compat import which from ..config import settings from .base import BaseEncodingBackend logger = logging.getLogger(__name__) RE_TIMECODE = re.compile(r'time=(\d+:\d+:\d+.\d+) ') console_encoding = locale.getdefaultlocale()[1] or 'UTF-8' class FFmpegBackend(BaseEncodingBackend): name = 'FFmpeg' def __init__(self): # This will fix errors in tests self.params = [ '-threads', str(settings.VIDEO_ENCODING_THREADS), '-y', # overwrite temporary created file '-strict', '-2', # support aac codec (which is experimental) ] self.ffmpeg_path = getattr( settings, 'VIDEO_ENCODING_FFMPEG_PATH', which('ffmpeg')) self.ffprobe_path = getattr( settings, 'VIDEO_ENCODING_FFPROBE_PATH', which('ffprobe')) if not self.ffmpeg_path: raise exceptions.FFmpegError("ffmpeg binary not found: {}".format( self.ffmpeg_path or '')) if not self.ffprobe_path: raise exceptions.FFmpegError("ffprobe binary not found: {}".format( self.ffmpeg_path or '')) @classmethod def check(cls): errors = super(FFmpegBackend, cls).check() try: FFmpegBackend() except exceptions.FFmpegError as e: errors.append(checks.Error( e.msg, hint="Please install ffmpeg.", obj=cls, id='video_conversion.E001', )) return errors def _spawn(self, cmds): try: return Popen( cmds, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True, ) except OSError as e: raise six.raise_from( exceptions.FFmpegError('Error while running ffmpeg binary'), e) def _check_returncode(self, process): stdout, stderr = process.communicate() if process.returncode != 0: raise exceptions.FFmpegError("`{}` exited with code {:d}".format( ' '.join(process.args), process.returncode)) self.stdout = stdout.decode(console_encoding) self.stderr = stderr.decode(console_encoding) return self.stdout, self.stderr # TODO reduce complexity def encode(self, source_path, target_path, params): # NOQA: C901 """ Encodes a video to a specified file. All encoder specific options are passed in using `params`. """ total_time = self.get_media_info(source_path)['duration'] cmds = [self.ffmpeg_path, '-i', source_path] cmds.extend(self.params) cmds.extend(params) cmds.extend([target_path]) process = self._spawn(cmds) buf = output = '' # update progress while True: # any more data? out = process.stderr.read(10) if not out: break out = out.decode(console_encoding) output += out buf += out try: line, buf = buf.split('\r', 1) except ValueError: continue try: time_str = RE_TIMECODE.findall(line)[0] except IndexError: continue # convert progress to percent time = 0 for part in time_str.split(':'): time = 60 * time + float(part) percent = time / total_time logger.debug('yield {}%'.format(percent)) yield percent if os.path.getsize(target_path) == 0: raise exceptions.FFmpegError("File size of generated file is 0") # wait for process to exit self._check_returncode(process) logger.debug(output) if not output: raise exceptions.FFmpegError("No output from FFmpeg.") yield 100 def _parse_media_info(self, data): media_info = json.loads(data) media_info['video'] = [stream for stream in media_info['streams'] if stream['codec_type'] == 'video'] media_info['audio'] = [stream for stream in media_info['streams'] if stream['codec_type'] == 'audio'] media_info['subtitle'] = [stream for stream in media_info['streams'] if stream['codec_type'] == 'subtitle'] del media_info['streams'] return media_info def get_media_info(self, video_path): """ Returns information about the given video as dict. """ cmds = [self.ffprobe_path, '-i', video_path] cmds.extend(['-print_format', 'json']) cmds.extend(['-show_format', '-show_streams']) process = self._spawn(cmds) stdout, __ = self._check_returncode(process) media_info = self._parse_media_info(stdout) return { 'duration': float(media_info['format']['duration']), 'width': int(media_info['video'][0]['width']), 'height': int(media_info['video'][0]['height']), } def get_thumbnail(self, video_path, at_time=0.5): """ Extracts an image of a video and returns its path. If the requested thumbnail is not within the duration of the video an `InvalidTimeError` is thrown. """ filename = os.path.basename(video_path) filename, __ = os.path.splitext(filename) _, image_path = tempfile.mkstemp(suffix='_{}.jpg'.format(filename)) video_duration = self.get_media_info(video_path)['duration'] if at_time > video_duration: raise exceptions.InvalidTimeError() thumbnail_time = at_time cmds = [self.ffmpeg_path, '-i', video_path, '-vframes', '1'] cmds.extend(['-ss', str(thumbnail_time), '-y', image_path]) process = self._spawn(cmds) self._check_returncode(process) if not os.path.getsize(image_path): # we somehow failed to generate thumbnail os.unlink(image_path) raise exceptions.InvalidTimeError() return image_path PK!:^!&&video_encoding/compat.pyfrom shutilwhich import which # NOQA PK!video_encoding/config.pyfrom appconf import AppConf from django.conf import settings # NOQA class VideoEncodingAppConf(AppConf): THREADS = 1 PROGRESS_UPDATE = 30 BACKEND = 'video_encoding.backends.ffmpeg.FFmpegBackend' BACKEND_PARAMS = {} FORMATS = { 'FFmpeg': [ { 'name': 'webm_sd', 'extension': 'webm', 'params': [ '-b:v', '1000k', '-maxrate', '1000k', '-bufsize', '2000k', '-codec:v', 'libvpx', '-r', '30', '-vf', 'scale=-1:480', '-qmin', '10', '-qmax', '42', '-codec:a', 'libvorbis', '-b:a', '128k', '-f', 'webm', ], }, { 'name': 'webm_hd', 'extension': 'webm', 'params': [ '-codec:v', 'libvpx', '-b:v', '3000k', '-maxrate', '3000k', '-bufsize', '6000k', '-vf', 'scale=-1:720', '-qmin', '11', '-qmax', '51', '-acodec', 'libvorbis', '-b:a', '128k', '-f', 'webm', ], }, { 'name': 'mp4_sd', 'extension': 'mp4', 'params': [ '-codec:v', 'libx264', '-crf', '20', '-preset', 'medium', '-b:v', '1000k', '-maxrate', '1000k', '-bufsize', '2000k', '-vf', 'scale=-2:480', # http://superuser.com/a/776254 '-codec:a', 'aac', '-b:a', '128k', '-strict', '-2', ], }, { 'name': 'mp4_hd', 'extension': 'mp4', 'params': [ '-codec:v', 'libx264', '-crf', '20', '-preset', 'medium', '-b:v', '3000k', '-maxrate', '3000k', '-bufsize', '6000k', '-vf', 'scale=-2:720', '-codec:a', 'aac', '-b:a', '128k', '-strict', '-2', ], }, ] } PK!صvideo_encoding/exceptions.pyclass VideoEncodingError(Exception): pass class FFmpegError(VideoEncodingError): def __init__(self, *args, **kwargs): self.msg = args[0] super(VideoEncodingError, self).__init__(*args, **kwargs) class InvalidTimeError(VideoEncodingError): pass PK! %Lvideo_encoding/fields.pyfrom django.db.models.fields.files import (FieldFile, ImageField, ImageFileDescriptor) from django.utils.translation import ugettext as _ from .backends import get_backend_class from .files import VideoFile class VideoFileDescriptor(ImageFileDescriptor): pass class VideoFieldFile(VideoFile, FieldFile): def delete(self, save=True): # Clear the video info cache if hasattr(self, '_info_cache'): del self._info_cache super(VideoFieldFile, self).delete(save=save) class VideoField(ImageField): attr_class = VideoFieldFile descriptor_class = VideoFileDescriptor description = _("Video") def __init__(self, verbose_name=None, name=None, duration_field=None, **kwargs): self.duration_field = duration_field super(VideoField, self).__init__(verbose_name, name, **kwargs) def check(self, **kwargs): errors = super(ImageField, self).check(**kwargs) errors.extend(self._check_backend()) return errors def _check_backend(self): backend = get_backend_class() return backend.check() def to_python(self, data): # use FileField method return super(ImageField, self).to_python(data) def update_dimension_fields(self, instance, force=False, *args, **kwargs): _file = getattr(instance, self.attname) # we need a real file if not _file._committed: return # write `width` and `height` super(VideoField, self).update_dimension_fields(instance, force, *args, **kwargs) if not self.duration_field: return # Nothing to update if we have no file and not being forced to update. if not _file and not force: return if getattr(instance, self.duration_field) and not force: return # get duration if file is defined duration = _file.duration if _file else None # update duration setattr(instance, self.duration_field, duration) def formfield(self, **kwargs): # use normal FileFieldWidget for now return super(ImageField, self).formfield(**kwargs) PK!Hvideo_encoding/files.pyimport os from django.core.files import File from .backends import get_backend class VideoFile(File): """ A mixin for use alongside django.core.files.base.File, which provides additional features for dealing with videos. """ def _get_width(self): """ Returns video width in pixels. """ return self._get_video_info().get('width', 0) width = property(_get_width) def _get_height(self): """ Returns video height in pixels. """ return self._get_video_info().get('height', 0) height = property(_get_height) def _get_duration(self): """ Returns duration in seconds. """ return self._get_video_info().get('duration', 0) duration = property(_get_duration) def _get_video_info(self): """ Returns basic information about the video as dictionary. """ if not hasattr(self, '_info_cache'): encoding_backend = get_backend() try: path = os.path.abspath(self.path) except AttributeError: path = os.path.abspath(self.name) self._info_cache = encoding_backend.get_media_info(path) return self._info_cache PK!4,]]video_encoding/manager.pyfrom django.db.models import Manager from django.db.models.query import QuerySet class FormatQuerySet(QuerySet): def in_progress(self): return self.filter(progress__lt=100) def complete(self): return self.filter(progress=100) class FormatManager(Manager.from_queryset(FormatQuerySet)): use_for_related_fields = True PK!(Y)video_encoding/migrations/0001_initial.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models import video_encoding.fields import video_encoding.models class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0002_remove_content_type_name'), ] operations = [ migrations.CreateModel( name='Format', fields=[ ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)), ('object_id', models.PositiveIntegerField()), ('field_name', models.CharField(max_length=255)), ('progress', models.PositiveSmallIntegerField(verbose_name='Progress', default=0)), ('format', models.CharField(verbose_name='Format', max_length=255)), ('file', video_encoding.fields.VideoField(height_field='height', verbose_name='File', width_field='width', max_length=2048, upload_to=video_encoding.models.upload_format_to)), ('width', models.PositiveIntegerField(verbose_name='Width', null=True)), ('height', models.PositiveIntegerField(verbose_name='Height', null=True)), ('duration', models.PositiveIntegerField(verbose_name='Duration (s)', null=True)), ('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)), ], options={ 'verbose_name': 'Format', 'verbose_name_plural': 'Formats', }, ), ] PK!D":video_encoding/migrations/0002_update_field_definitions.py# Generated by Django 2.1.3 on 2018-11-16 00:48 from django.db import migrations, models import django.db.models.deletion import video_encoding.fields import video_encoding.models class Migration(migrations.Migration): dependencies = [ ('video_encoding', '0001_initial'), ] operations = [ migrations.AlterField( model_name='format', name='content_type', field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), ), migrations.AlterField( model_name='format', name='duration', field=models.PositiveIntegerField(editable=False, null=True, verbose_name='Duration (s)'), ), migrations.AlterField( model_name='format', name='file', field=video_encoding.fields.VideoField(editable=False, height_field='height', max_length=2048, upload_to=video_encoding.models.upload_format_to, verbose_name='File', width_field='width'), ), migrations.AlterField( model_name='format', name='format', field=models.CharField(editable=False, max_length=255, verbose_name='Format'), ), migrations.AlterField( model_name='format', name='height', field=models.PositiveIntegerField(editable=False, null=True, verbose_name='Height'), ), migrations.AlterField( model_name='format', name='object_id', field=models.PositiveIntegerField(editable=False), ), migrations.AlterField( model_name='format', name='progress', field=models.PositiveSmallIntegerField(default=0, editable=False, verbose_name='Progress'), ), migrations.AlterField( model_name='format', name='width', field=models.PositiveIntegerField(editable=False, null=True, verbose_name='Width'), ), ] PK!%video_encoding/migrations/__init__.pyPK!3video_encoding/models.pyfrom os.path import splitext from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.translation import ugettext_lazy as _ from .fields import VideoField from .manager import FormatManager def upload_format_to(i, f): return 'formats/%s/%s%s' % ( i.format, splitext(getattr(i.video, i.field_name).name)[0], # keep path splitext(f)[1].lower()) class Format(models.Model): object_id = models.PositiveIntegerField( editable=False, ) content_type = models.ForeignKey( ContentType, editable=False, on_delete=models.CASCADE ) video = GenericForeignKey() field_name = models.CharField( max_length=255, ) progress = models.PositiveSmallIntegerField( default=0, editable=False, verbose_name=_("Progress"), ) format = models.CharField( max_length=255, editable=False, verbose_name=_("Format"), ) file = VideoField( duration_field='duration', editable=False, max_length=2048, upload_to=upload_format_to, verbose_name=_("File"), width_field='width', height_field='height', ) width = models.PositiveIntegerField( editable=False, null=True, verbose_name=_("Width"), ) height = models.PositiveIntegerField( editable=False, null=True, verbose_name=_("Height"), ) duration = models.PositiveIntegerField( editable=False, null=True, verbose_name=_("Duration (s)"), ) objects = FormatManager() class Meta: verbose_name = _("Format") verbose_name_plural = _("Formats") def __str__(self): return '{} ({:d}%)'.format(self.file.name, self.progress) def unicode(self): return self.__str__() def update_progress(self, percent, commit=True): if 0 > percent > 100: raise ValueError("Invalid percent value.") self.progress = percent if commit: self.save() def reset_progress(self, commit=True): self.percent = 0 if commit: self.save() PK! video_encoding/tasks.pyimport os import tempfile from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.core.files import File from .backends import get_backend from .config import settings from .exceptions import VideoEncodingError from .fields import VideoField from .models import Format def convert_all_videos(app_label, model_name, object_pk): """ Automatically converts all videos of a given instance. """ # get instance Model = apps.get_model(app_label=app_label, model_name=model_name) instance = Model.objects.get(pk=object_pk) # search for `VideoFields` fields = instance._meta.fields for field in fields: if isinstance(field, VideoField): if not getattr(instance, field.name): # ignore empty fields continue # trigger conversion fieldfile = getattr(instance, field.name) convert_video(fieldfile) def convert_video(fieldfile, force=False): """ Converts a given video file into all defined formats. """ instance = fieldfile.instance field = fieldfile.field filename = os.path.basename(fieldfile.path) source_path = fieldfile.path encoding_backend = get_backend() for options in settings.VIDEO_ENCODING_FORMATS[encoding_backend.name]: video_format, created = Format.objects.get_or_create( object_id=instance.pk, content_type=ContentType.objects.get_for_model(instance), field_name=field.name, format=options['name']) # do not reencode if not requested if video_format.file and not force: continue else: # set progress to 0 video_format.reset_progress() # TODO do not upscale videos _, target_path = tempfile.mkstemp( suffix='_{name}.{extension}'.format(**options)) try: encoding = encoding_backend.encode( source_path, target_path, options['params']) while encoding: try: progress = next(encoding) except StopIteration: break video_format.update_progress(progress) except VideoEncodingError: # TODO handle with more care video_format.delete() os.remove(target_path) continue # save encoded file video_format.file.save( '{filename}_{name}.{extension}'.format(filename=filename, **options), File(open(target_path, mode='rb'))) video_format.update_progress(100) # now we are ready # remove temporary file os.remove(target_path) PK!nN-django_video_encoding-0.4.0.dist-info/LICENSECopyright (c) 2016, Alexander Frenzel All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Alexander Frenzel nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. PK!H9VWX+django_video_encoding-0.4.0.dist-info/WHEEL A н#f."jm)!fb҅~ܴA,mTD}E n0H饹*|D[¬c i=0(q3PK!Hi Q .django_video_encoding-0.4.0.dist-info/METADATAYo8~̫]:6 \B͍$$e[~3$%?8fF+RaEh#UgG0(rQLTo&SP=(b55?<z 5BVfKT1mBU9"dACaX\XD2 FeEAGURLPOV#|L"JH ( Z#ERϝsί hzU*_*5EbjWίڕd",X]BIr7j tZaw HK/qRzӋoyu+T):Wc;OQi%>*20?6- y:oOA#~IZ,Ss"H޴)v6dY2ɟ ? }\UIEa~.09kceAJ/^. :c-KRwlaQ"YxO=}vQ99!e>f*!K j\r֮(7IӷBO.-^JOdhmU4A4"˜4\ǃ0gaԋ"b''bxIEfd!=|h܁7s<".W)dե:ζ9d]N7J;Kݱr3)xP8h$BG*\.V˧tUd0Znbʼ{rRW-L7cLاppBPW(1Z: vE2zo΅aU"gݪ8HeU&3bvM *X4-!T u) Nx*HzLnPZ\4(]?A虒y滪ib:keŦ0$_]{4[q؂ٚsBgbٷu1#`Hvz E9 P\ʧ\h*\h3!9weG Bg:^!0ИiM!XUGvr]d>s+Ihx;[-hcYtu~q_|<9.~xO`&jX J"$'MKas`DB"ӔӋs:8 uv6V`8/a//Stt?*e| 5 ڠ0]c E0%=]nc1Ґa"x@W4 ڡ  <؍=@[YkD.q 7tG$! t8'l%Ư~Ϡfc?9#=3NCRdM=#_gY/[l8(M24 tmDކ-yoԟsxppH_zxe(}?aAijhV>=>xlL'!8_r}!~ 7ċZ#i/ 'i?ku@c߈EKݨ?}ll W!cl#[x{ ?bqx~o/C",ΰ ::b]C`,ի'TBSDky{wsz&4XbtQt#Qb "LTK?+` YҚMk ̒0O:ׂZd+idF'hn\RQrM*%0Uhq,_c@Q!dF" ƕFFz'܋.k+i0lL~uuHl%K 9$m5 =":؂G+=3p8:쾭Ycp%5LQ~k8Zuޑ쥵sӅ孽}7 _y~^#=ך(IФT`Qڰu9` ;*>Q 3V-:h$B7oFAT5D] Τ09߽\UlYBl*iam =PK!H_?,django_video_encoding-0.4.0.dist-info/RECORDْH~d 6BQ"Rdݧ"zj'b^OG(uNPGPEo͂u9')\txj3 F*? #Fz )Qw*GE,u}%(šY{h;UۜuCz.NӇ/6ebѷeJ'XbG :& B+J*ich=?'a3xaќ0Ȧi `?҉_(5.u7N]Bا+/rU"uـ )h~ EQNcd$K!B^V+Wy>. aVIpxˌ7.dsYlǀE"9M{B*ި[Nhw+ysz4  QSɳG*n~Я*dϙpwkw'_h+x~u#ТM@8<]%AK1b߽T 'V5Icޕ #zweh >".e7ǎ#p} 8QZ̼O/Nc*Ab$þk Mz}:J`]I2>¡9F膯%r1ݜ ZA{TGNY'<,6m2ݐcΫD{RL7etų H|5k)*V8p'#aS?̭~$Peuʼn7xKPJwSUaլwʍl2`ﴣ~[#mVKqo 8rIRdj6pv-Լlkic,f"R;^Z3kKt[R}xǨ0.^jXtS}^w{~PK!video_encoding/__init__.pyPK! ?8video_encoding/admin.pyPK![[# video_encoding/backends/__init__.pyPK!5=video_encoding/backends/base.pyPK!sb!video_encoding/backends/ffmpeg.pyPK!:^!&&!video_encoding/compat.pyPK!'"video_encoding/config.pyPK!ص-*video_encoding/exceptions.pyPK! %L{+video_encoding/fields.pyPK!H4video_encoding/files.pyPK!4,]]9video_encoding/manager.pyPK!(Y)A;video_encoding/migrations/0001_initial.pyPK!D":Avideo_encoding/migrations/0002_update_field_definitions.pyPK!%Ivideo_encoding/migrations/__init__.pyPK!3+Jvideo_encoding/models.pyPK! HSvideo_encoding/tasks.pyPK!nN-R^django_video_encoding-0.4.0.dist-info/LICENSEPK!H9VWX+ddjango_video_encoding-0.4.0.dist-info/WHEELPK!Hi Q .Redjango_video_encoding-0.4.0.dist-info/METADATAPK!H_?,pdjango_video_encoding-0.4.0.dist-info/RECORDPK)mt