用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程序运行效率的6个方法
Mar 31 Python
Python导出数据到Excel可读取的CSV文件的方法
May 12 Python
十个Python程序员易犯的错误
Dec 15 Python
Python在图片中添加文字的两种方法
Apr 29 Python
python flask实现分页效果
Jun 27 Python
python实现微信发送邮件关闭电脑功能
Feb 22 Python
python分批定量读取文件内容,输出到不同文件中的方法
Dec 08 Python
python3 字符串/列表/元组(str/list/tuple)相互转换方法及join()函数的使用
Apr 03 Python
基于腾讯云服务器部署微信小程序后台服务(Python+Django)
May 08 Python
flask 框架操作MySQL数据库简单示例
Feb 02 Python
Python Scrapy图片爬取原理及代码实例
Jun 12 Python
Scrapy基于scrapy_redis实现分布式爬虫部署的示例
Sep 29 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
使用PHP实现密保卡功能实现代码&amp;lt;打包下载直接运行&amp;gt;
2011/10/09 PHP
浅谈web上存漏洞及原理分析、防范方法(安全文件上存方法)
2013/06/29 PHP
PDO防注入原理分析以及使用PDO的注意事项总结
2014/10/23 PHP
浅谈PHP面向对象之访问者模式+组合模式
2017/05/22 PHP
Jquery中删除元素的实现代码
2011/12/29 Javascript
js数组的操作详解
2013/03/27 Javascript
javascript实现数组中的内容随机输出
2015/08/11 Javascript
简单理解vue中Props属性
2016/10/27 Javascript
jQuery层级选择器_动力节点节点Java学院整理
2017/07/04 jQuery
Vue异步加载about组件
2017/10/31 Javascript
Layui给数据表格动态添加一行并跳转到添加行所在页的方法
2018/08/20 Javascript
解决layui轮播图有数据不显示的情况
2019/09/16 Javascript
原生js+canvas实现下雪效果
2020/08/02 Javascript
[01:56]2014DOTA2西雅图邀请赛 MVP外卡赛老队长精辟点评
2014/07/09 DOTA
[01:12]快闪回顾DOTA2亚洲邀请赛(DAC) 静候2018新征程开启
2018/03/11 DOTA
用Python的Django框架完成视频处理任务的教程
2015/04/02 Python
python获取指定网页上所有超链接的方法
2015/04/04 Python
状态机的概念和在Python下使用状态机的教程
2015/04/11 Python
Python可变参数函数用法实例
2015/07/07 Python
使用Pyinstaller的最新踩坑实战记录
2017/11/08 Python
Python Matplotlib库安装与基本作图示例
2019/01/09 Python
tensorflow之并行读入数据详解
2020/02/05 Python
Python ORM框架Peewee用法详解
2020/04/29 Python
马来西亚综合购物网站:Lazada马来西亚
2018/06/05 全球购物
char型变量中能不能存贮一个中文汉字
2015/07/08 面试题
生物专业个人自荐信范文
2013/11/29 职场文书
四好少年事迹材料
2014/01/12 职场文书
国际贸易专业个人鉴定
2014/02/22 职场文书
《蓝色的树叶》教学反思
2014/02/24 职场文书
劳动工资科岗位职责范本
2014/03/02 职场文书
《赶海》教学反思
2014/04/20 职场文书
党的生日活动方案
2014/08/15 职场文书
县委党的群众路线教育实践活动工作情况报告
2014/10/25 职场文书
员工工作及收入证明
2014/10/28 职场文书
java多态注意项小结
2021/10/16 Java/Android
MySQL 数据库 增删查改、克隆、外键 等操作
2022/05/11 MySQL