用Python的Django框架完成视频处理任务的教程


Posted in Python onApril 02, 2015

Stickyworld 的网页应用已经支持视频拨放一段时间,但都是通过YouTube的嵌入模式实现。我们开始提供新的版本支持视频操作,可以让我们的用户不用受制于YouTube的服务。

我过去曾经参与过一个项目,客户需要视频转码功能,这实在不是个容易达成的需求。需要大量的读取每一个视频、音讯与视频容器的格式再输出符合网页使用与喜好的视频格式。

考虑到这一点,我们决定将转码的工作交给 Encoding.com 。这个网站可以免费让你编码1GB大小的视频,超过1GB容量的文件将采取分级计价收费。

开发的代码如下,我上传了一个178KB容量的两秒视频来测试代码是否成功运作。当测试过程没有发生任何的例外错误后,我继续测试其它更大的外部文件。

 
阶段一:用户上传视频文件

现在这的新的代码段提供了一个基于 HTML5且可以快速上手的 的上传机制。用CoffeeScript撰写的代码,可以从客户端上传文件到服务器端。
 

$scope.upload_slide = (upload_slide_form) ->
  file = document.getElementById("slide_file").files[0]
  reader = new FileReader()
  reader.readAsDataURL file
  reader.onload = (event) ->
   result = event.target.result
   fileName = document.getElementById("slide_file").files[0].name
   $.post "/world/upload_slide",
    data: result
    name: fileName
    room_id: $scope.room.id
    (response_data) ->
     if response_data.success? is not yes
      console.error "There was an error uploading the file", response_data
     else
      console.log "Upload successful", response_data
  reader.onloadstart = ->
   console.log "onloadstart"
  reader.onprogress = (event) ->
   console.log "onprogress", event.total, event.loaded, (event.loaded / event.total) * 100
  reader.onabort = ->
   console.log "onabort"
  reader.onerror = ->
   console.log "onerror"
  reader.onloadend = (event) ->
   console.log "onloadend", event

最好可以通过 (“slide_file”).files 且经由独立的POST上传每个文件,而不是由一个POST需求上传所有文件。稍后我们会解释这点。

 
阶段二:验证并上传至 Amazon S3

后端我们运行了Django与RabbitMQ。主要的模块如下:
 

$ pip install 'Django>=1.5.2' 'django-celery>=3.0.21' \
  'django-storages>=1.1.8' 'lxml>=3.2.3' 'python-magic>=0.4.3'

我建立了两个模块:SlideUploadQueue 用来储存每一次上传的数据,SlideVideoMedia 则是用来储存每个要上传影片的数据。
 
class SlideUploadQueue(models.Model):
  created_by = models.ForeignKey(User)
  created_time = models.DateTimeField(db_index=True)
  original_file = models.FileField(
    upload_to=filename_sanitiser, blank=True, default='')
  media_type = models.ForeignKey(MediaType)
  encoding_com_tracking_code = models.CharField(
    default='', max_length=24, blank=True)
 
  STATUS_AWAITING_DATA = 0
  STATUS_AWAITING_PROCESSING = 1
  STATUS_PROCESSING = 2
  STATUS_AWAITING_3RD_PARTY_PROCESSING = 5
  STATUS_FINISHED = 3
  STATUS_FAILED = 4
 
  STATUS_LIST = (
    (STATUS_AWAITING_DATA, 'Awaiting Data'),
    (STATUS_AWAITING_PROCESSING, 'Awaiting processing'),
    (STATUS_PROCESSING, 'Processing'),
    (STATUS_AWAITING_3RD_PARTY_PROCESSING,
      'Awaiting 3rd-party processing'),
    (STATUS_FINISHED, 'Finished'),
    (STATUS_FAILED, 'Failed'),
  )
 
  status = models.PositiveSmallIntegerField(
    default=STATUS_AWAITING_DATA, choices=STATUS_LIST)
 
  class Meta:
    verbose_name = 'Slide'
    verbose_name_plural = 'Slide upload queue'
 
  def save(self, *args, **kwargs):
    if not self.created_time:
      self.created_time = \
        datetime.utcnow().replace(tzinfo=pytz.utc)
 
    return super(SlideUploadQueue, self).save(*args, **kwargs)
 
  def __unicode__(self):
    if self.id is None:
      return 'new <SlideUploadQueue>'
    return '<SlideUploadQueue> %d' % self.id
 
class SlideVideoMedia(models.Model):
  converted_file = models.FileField(
    upload_to=filename_sanitiser, blank=True, default='')
 
  FORMAT_MP4 = 0
  FORMAT_WEBM = 1
  FORMAT_OGG = 2
  FORMAT_FL9 = 3
  FORMAT_THUMB = 4
 
  supported_formats = (
    (FORMAT_MP4, 'MPEG 4'),
    (FORMAT_WEBM, 'WebM'),
    (FORMAT_OGG, 'OGG'),
    (FORMAT_FL9, 'Flash 9 Video'),
    (FORMAT_THUMB, 'Thumbnail'),
  )
 
  mime_types = (
    (FORMAT_MP4, 'video/mp4'),
    (FORMAT_WEBM, 'video/webm'),
    (FORMAT_OGG, 'video/ogg'),
    (FORMAT_FL9, 'video/mp4'),
    (FORMAT_THUMB, 'image/jpeg'),
  )
 
  format = models.PositiveSmallIntegerField(
    default=FORMAT_MP4, choices=supported_formats)
 
  class Meta:
    verbose_name = 'Slide video'
    verbose_name_plural = 'Slide videos'
 
  def __unicode__(self):
    if self.id is None:
      return 'new <SlideVideoMedia>'
    return '<SlideVideoMedia> %d' % self.id

我们的模块皆使用 filename_sanitiser。FileField 自动的将文件名调整成 <model>/<uuid4>.<extention> 格式。整理每个文件名并确保其独一性。我们采用了有时效性签署的网址列让我们可以掌控哪些使用者在使用我们的服务,使用了多久。
 

def filename_sanitiser(instance, filename):
  folder = instance.__class__.__name__.lower()
  ext = 'jpg'
 
  if '.' in filename:
    t_ext = filename.split('.')[-1].strip().lower()
 
    if t_ext != '':
      ext = t_ext
 
  return '%s/%s.%s' % (folder, str(uuid.uuid4()), ext)

拿来测试的文件 testing.mov 将会转换成以下网址:https://our-bucket.s3.amazonaws.com/slideuploadqueue/3fe27193-e87f-4244-9aa2-66409f70ebd3.mov 并经由Django Storages 模块上传。

我们通过 Magic 验证从使用者端浏览器上传的文件。Magic可以从文件内容侦测是何种类型的文件。
 

@verify_auth_token
@return_json
def upload_slide(request):
  file_data = request.POST.get('data', '')
  file_data = base64.b64decode(file_data.split(';base64,')[1])
  description = magic.from_buffer(file_data)

如果文件类型符合MPEG v4 系统或是Apple QuickTime 电影,我们就知道该文件转码不会有太大问题。如果格式不是上述所提的几种,我们会标志给用户知悉。
接着,我们将通过SlideUploadQueue 模块将视频储存到队列并发送一个需求给 RabbitMQ。因为我们使用了Django Storages 模块,文件将自动被上传到 Amazon S3。
 

slide_upload = SlideUploadQueue()
...
slide_upload.status = SlideUploadQueue.STATUS_AWAITING_PROCESSING
slide_upload.save()
slide_upload.original_file.\
  save('anything.%s' % file_ext, ContentFile(file_data))
slide_upload.save()
 
task = ConvertRawSlideToSlide()
task.delay(slide_upload)

阶段3:发送视频到第三方.

RabbitMQ 将控管 task.delay(slide_upload) 的呼叫。
我们现在只需要发送视频档网址与输出格式给Encoding.com。该网站会回复我们一个工作码让我们检查视频转码的进度。

class ConvertRawSlideToSlide(Task):
  queue = 'backend_convert_raw_slides'
  ...
  def _handle_video(self, slide_upload):
    mp4 = {
      'output': 'mp4',
      'size': '320x240',
      'bitrate': '256k',
      'audio_bitrate': '64k',
      'audio_channels_number': '2',
      'keep_aspect_ratio': 'yes',
      'video_codec': 'mpeg4',
      'profile': 'main',
      'vcodecparameters': 'no',
      'audio_codec': 'libfaac',
      'two_pass': 'no',
      'cbr': 'no',
      'deinterlacing': 'no',
      'keyframe': '300',
      'audio_volume': '100',
      'file_extension': 'mp4',
      'hint': 'no',
    }
 
    webm = {
      'output': 'webm',
      'size': '320x240',
      'bitrate': '256k',
      'audio_bitrate': '64k',
      'audio_sample_rate': '44100',
      'audio_channels_number': '2',
      'keep_aspect_ratio': 'yes',
      'video_codec': 'libvpx',
      'profile': 'baseline',
      'vcodecparameters': 'no',
      'audio_codec': 'libvorbis',
      'two_pass': 'no',
      'cbr': 'no',
      'deinterlacing': 'no',
      'keyframe': '300',
      'audio_volume': '100',
      'preset': '6',
      'file_extension': 'webm',
      'acbr': 'no',
    }
 
    ogg = {
      'output': 'ogg',
      'size': '320x240',
      'bitrate': '256k',
      'audio_bitrate': '64k',
      'audio_sample_rate': '44100',
      'audio_channels_number': '2',
      'keep_aspect_ratio': 'yes',
      'video_codec': 'libtheora',
      'profile': 'baseline',
      'vcodecparameters': 'no',
      'audio_codec': 'libvorbis',
      'two_pass': 'no',
      'cbr': 'no',
      'deinterlacing': 'no',
      'keyframe': '300',
      'audio_volume': '100',
      'file_extension': 'ogg',
      'acbr': 'no',
    }
 
    flv = {
      'output': 'fl9',
      'size': '320x240',
      'bitrate': '256k',
      'audio_bitrate': '64k',
      'audio_channels_number': '2',
      'keep_aspect_ratio': 'yes',
      'video_codec': 'libx264',
      'profile': 'high',
      'vcodecparameters': 'no',
      'audio_codec': 'libfaac',
      'two_pass': 'no',
      'cbr': 'no',
      'deinterlacing': 'no',
      'keyframe': '300',
      'audio_volume': '100',
      'file_extension': 'mp4',
    }
 
    thumbnail = {
      'output': 'thumbnail',
      'time': '5',
      'video_codec': 'mjpeg',
      'keep_aspect_ratio': 'yes',
      'file_extension': 'jpg',
    }
 
    encoder = Encoding(settings.ENCODING_API_USER_ID,
      settings.ENCODING_API_USER_KEY)
    resp = encoder.add_media(source=[slide_upload.original_file.url],
      formats=[mp4, webm, ogg, flv, thumbnail])
 
    media_id = None
 
    if resp is not None and resp.get('response') is not None:
      media_id = resp.get('response').get('MediaID')
 
    if media_id is None:
      slide_upload.status = SlideUploadQueue.STATUS_FAILED
      slide_upload.save()
      log.error('Unable to communicate with encoding.com')
      return False
 
    slide_upload.encoding_com_tracking_code = media_id
    slide_upload.status = \
      SlideUploadQueue.STATUS_AWAITING_3RD_PARTY_PROCESSING
    slide_upload.save()
    return True

Encoding.com 推荐一些堪用的Python程序,可用来与它们的服务沟通。我修改了模块一些地方,但还需要修改一些功能才能达到我满意的状态。以下是修改过后目前正在使用的程序代码:

import httplib
from lxml import etree
import urllib
from xml.parsers.expat import ExpatError
import xmltodict
 
ENCODING_API_URL = 'manage.encoding.com:80'
 
class Encoding(object):
 
  def __init__(self, userid, userkey, url=ENCODING_API_URL):
    self.url = url
    self.userid = userid
    self.userkey = userkey
 
  def get_media_info(self, action='GetMediaInfo', ids=[],
    headers={'Content-Type': 'application/x-www-form-urlencoded'}):
    query = etree.Element('query')
 
    nodes = {
      'userid': self.userid,
      'userkey': self.userkey,
      'action': action,
      'mediaid': ','.join(ids),
    }
 
    query = self._build_tree(etree.Element('query'), nodes)
    results = self._execute_request(query, headers)
 
    return self._parse_results(results)
 
  def get_status(self, action='GetStatus', ids=[], extended='no',
    headers={'Content-Type': 'application/x-www-form-urlencoded'}):
    query = etree.Element('query')
 
    nodes = {
      'userid': self.userid,
      'userkey': self.userkey,
      'action': action,
      'extended': extended,
      'mediaid': ','.join(ids),
    }
 
    query = self._build_tree(etree.Element('query'), nodes)
    results = self._execute_request(query, headers)
 
    return self._parse_results(results)
 
  def add_media(self, action='AddMedia', source=[], notify='', formats=[],
    instant='no',
    headers={'Content-Type': 'application/x-www-form-urlencoded'}):
    query = etree.Element('query')
 
    nodes = {
      'userid': self.userid,
      'userkey': self.userkey,
      'action': action,
      'source': source,
      'notify': notify,
      'instant': instant,
    }
 
    query = self._build_tree(etree.Element('query'), nodes)
 
    for format in formats:
      format_node = self._build_tree(etree.Element('format'), format)
      query.append(format_node)
 
    results = self._execute_request(query, headers)
    return self._parse_results(results)
 
  def _build_tree(self, node, data):
    for k, v in data.items():
      if isinstance(v, list):
        for item in v:
          element = etree.Element(k)
          element.text = item
          node.append(element)
      else:
        element = etree.Element(k)
        element.text = v
        node.append(element)
 
    return node
 
  def _execute_request(self, xml, headers, path='', method='POST'):
    params = urllib.urlencode({'xml': etree.tostring(xml)})
 
    conn = httplib.HTTPConnection(self.url)
    conn.request(method, path, params, headers)
    response = conn.getresponse()
    data = response.read()
    conn.close()
    return data
 
  def _parse_results(self, results):
    try:
      return xmltodict.parse(results)
    except ExpatError, e:
      print 'Error parsing encoding.com response'
      print e
      return None

其他待完成事项包括通过HTTPS-only (加密联机) 使用Encoding.com 严谨的SSL验证,还有一些单元测试。
阶段4:下载所有新的视频档格式

我们有个定期执行的程序,通过RabbitMQ每15秒检查视频转码的进度:
 

class CheckUpOnThirdParties(PeriodicTask):
  run_every = timedelta(seconds=settings.THIRD_PARTY_CHECK_UP_INTERVAL)
  ...
  def _handle_encoding_com(self, slides):
    format_lookup = {
      'mp4': SlideVideoMedia.FORMAT_MP4,
      'webm': SlideVideoMedia.FORMAT_WEBM,
      'ogg': SlideVideoMedia.FORMAT_OGG,
      'fl9': SlideVideoMedia.FORMAT_FL9,
      'thumbnail': SlideVideoMedia.FORMAT_THUMB,
    }
 
    encoder = Encoding(settings.ENCODING_API_USER_ID,
      settings.ENCODING_API_USER_KEY)
 
    job_ids = [item.encoding_com_tracking_code for item in slides]
    resp = encoder.get_status(ids=job_ids)
 
    if resp is None:
      log.error('Unable to check up on encoding.com')
      return False

检查Encoding.com的响应来验证每个部分是否正确以利我们继续下去。

if resp.get('response') is None:
  log.error('Unable to get response node from encoding.com')
  return False
 
resp_id = resp.get('response').get('id')
 
if resp_id is None:
  log.error('Unable to get media id from encoding.com')
  return False
 
slide = SlideUploadQueue.objects.filter(
  status=SlideUploadQueue.STATUS_AWAITING_3RD_PARTY_PROCESSING,
  encoding_com_tracking_code=resp_id)
 
if len(slide) != 1:
  log.error('Unable to find a single record for %s' % resp_id)
  return False
 
resp_status = resp.get('response').get('status')
 
if resp_status is None:
  log.error('Unable to get status from encoding.com')
  return False
 
if resp_status != u'Finished':
  log.debug("%s isn't finished, will check back later" % resp_id)
  return True
 
formats = resp.get('response').get('format')
 
if formats is None:
  log.error("No output formats were found. Something's wrong.")
  return False
 
for format in formats:
  try:
    assert format.get('status') == u'Finished', \
    "%s is not finished. Something's wrong." % format.get('id')
 
    output = format.get('output')
    assert output in ('mp4', 'webm', 'ogg', 'fl9',
      'thumbnail'), 'Unknown output format %s' % output
 
    s3_dest = format.get('s3_destination')
    assert 'http://encoding.com.result.s3.amazonaws.com/'\
      in s3_dest, 'Suspicious S3 url: %s' % s3_dest
 
    https_link = \
      'https://s3.amazonaws.com/encoding.com.result/%s' %\
      s3_dest.split('/')[-1]
    file_ext = https_link.split('.')[-1].strip()
 
    assert len(file_ext) > 0,\
      'Unable to get file extension from %s' % https_link
 
    count = SlideVideoMedia.objects.filter(slide_upload=slide,
      format=format_lookup[output]).count()
 
    if count != 0:
      print 'There is already a %s file for this slide' % output
      continue
 
    content = self.download_content(https_link)
 
    assert content is not None,\
      'There is no content for %s' % format.get('id')
  except AssertionError, e:
    log.error('A format did not pass all assertions: %s' % e)
    continue

到这里我们已确认所有事项皆正常,所以我们可以储存所有的视频档了:
 

media = SlideVideoMedia()
media.format = format_lookup[output]
media.converted_file.save('blah.%s' % file_ext, ContentFile(content))
media.save()

阶段5:经由HTML5播放视频档

在我们的前端网页已经新增了一个有HTML5的影像单元的网页。并采用对每个浏览器都有最佳支持的video.js来显示视频。
 

? bower install video.js
bower caching git://github.com/videojs/video.js-component.git
bower cloning git://github.com/videojs/video.js-component.git
bower fetching video.js
bower checking out video.js#v4.0.3
bower copying /home/mark/.bower/cache/video.js/5ab058cd60c5615aa38e8e706cd0f307
bower installing video.js#4.0.3

在我们的首页有包含其他相依的文件:
 

!!! 5
html(lang="en", class="no-js")
 head
  meta(http-equiv='Content-Type', content='text/html; charset=UTF-8')
  ...
  link(rel='stylesheet', type='text/css', href='/components/video-js-4.1.0/video-js.css')
  script(type='text/javascript', src='/components/video-js-4.1.0/video.js')

在Angular.js/JADE-based 框架下的模块,我们引入<video>卷标 与其<source>子卷标。每个视频文件都会有缩图通过<video>卷标的 poster 组件显示,缩图的图像是由我们从视频的前几秒撷取下来。
 

#main.span12
  video#example_video_1.video-js.vjs-default-skin(controls, preload="auto", width="640", height="264", poster="{{video_thumbnail}}", data-setup='{"example_option":true}', ng-show="videos")
    source(ng-repeat="video in videos", src="{{video.src}}", type="{{video.type}}")

还会显示出我们转换的每个视频文件格式,并使用在<source>标签。Video.js 会根据使用者使用的浏览器决定播放哪种格式的视频。

我们仍然有许多工作需要完成,建立单元测试与加强和Encoding.com服务沟通的程序。如果你对这些工作感兴趣请与我连络。

Python 相关文章推荐
Python、Javascript中的闭包比较
Feb 04 Python
在Python中使用cookielib和urllib2配合PyQuery抓取网页信息
Apr 25 Python
Python使用ntplib库同步校准当地时间的方法
Jul 02 Python
教你用Python写安卓游戏外挂
Jan 11 Python
Flask web开发处理POST请求实现(登录案例)
Jul 26 Python
python制作抖音代码舞
Apr 07 Python
python3常用的数据清洗方法(小结)
Oct 31 Python
Python while循环使用else语句代码实例
Feb 07 Python
Python json转字典字符方法实例解析
Apr 13 Python
Python ORM框架Peewee用法详解
Apr 29 Python
Python闭包及装饰器运行原理解析
Jun 17 Python
python字典key不能是可以是啥类型
Aug 04 Python
用map函数来完成Python并行任务的简单示例
Apr 02 #Python
对于Python异常处理慎用“except:pass”建议
Apr 02 #Python
Python的设计模式编程入门指南
Apr 02 #Python
介绍Python中的一些高级编程技巧
Apr 02 #Python
用Python代码来解图片迷宫的方法整理
Apr 02 #Python
在Python3中使用asyncio库进行快速数据抓取的教程
Apr 02 #Python
Python中的Classes和Metaclasses详解
Apr 02 #Python
You might like
解析将多维数组转换为支持curl提交的一维数组格式
2013/07/08 PHP
php创建和删除目录函数介绍和递归删除目录函数分享
2014/11/18 PHP
PHP中的类型约束介绍
2015/05/11 PHP
PHP弹出对话框技巧详细解读
2015/09/26 PHP
PHP验证码类ValidateCode解析
2017/01/07 PHP
php生成毫秒时间戳的实例讲解
2017/09/22 PHP
利用PHP判断是手机移动端还是PC端访问的函数示例
2017/12/14 PHP
JavaScript 函数调用规则
2009/09/14 Javascript
Three.js源码阅读笔记(Object3D类)
2012/12/27 Javascript
js showModalDialog参数的使用详解
2014/01/07 Javascript
Javascript玩转继承(二)
2014/05/08 Javascript
技术男用来对妹子表白的百度首页
2014/07/23 Javascript
javascript跨域的方法汇总
2015/10/23 Javascript
JavaScript模版引擎的基本实现方法浅析
2016/02/15 Javascript
nodejs对express中next函数的一些理解
2017/09/08 NodeJs
Vue中v-for的数据分组实例
2018/03/07 Javascript
Element Rate 评分的使用方法
2020/07/27 Javascript
JS将指定的某个字符全部转换为其他字符实例代码
2020/10/13 Javascript
[01:36:17]DOTA2-DPC中国联赛 正赛 Ehome vs iG BO3 第一场 1月31日
2021/03/11 DOTA
Python3安装pip工具的详细步骤
2019/10/14 Python
python 爬虫百度地图的信息界面的实现方法
2019/10/27 Python
Python编程快速上手——Excel表格创建乘法表案例分析
2020/02/28 Python
python实现b站直播自动发送弹幕功能
2021/02/20 Python
HTML5 Canvas中使用路径描画二阶、三阶贝塞尔曲线
2015/01/01 HTML / CSS
英国自行车商店:AW Cycles
2021/02/24 全球购物
C#面试题问题集
2016/04/02 面试题
AJax面试题
2014/11/25 面试题
JAVA高级程序员面试题
2013/09/06 面试题
母亲节感恩寄语
2014/02/21 职场文书
旅行社各个岗位职责
2014/03/15 职场文书
公开服务承诺制度
2014/03/26 职场文书
爱之链教学反思
2014/04/30 职场文书
暑期培训班招生方案
2014/08/26 职场文书
奉献家乡演讲稿
2014/09/16 职场文书
学校2016年九九重阳节活动总结
2016/04/01 职场文书
给原生html中添加水印遮罩层的实现示例
2021/04/02 Javascript