<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>润物无声 &#187; Video</title>
	<atom:link href="http://blog.zhourunsheng.com/tag/video/feed/" rel="self" type="application/rss+xml" />
	<link>http://blog.zhourunsheng.com</link>
	<description>天空一朵雨做的云</description>
	<lastBuildDate>Sat, 08 May 2021 05:17:21 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=4.1.41</generator>
	<item>
		<title>搭建基于Python的视频服务器</title>
		<link>http://blog.zhourunsheng.com/2013/08/%e6%90%ad%e5%bb%ba%e5%9f%ba%e4%ba%8epython%e7%9a%84%e8%a7%86%e9%a2%91%e6%9c%8d%e5%8a%a1%e5%99%a8/</link>
		<comments>http://blog.zhourunsheng.com/2013/08/%e6%90%ad%e5%bb%ba%e5%9f%ba%e4%ba%8epython%e7%9a%84%e8%a7%86%e9%a2%91%e6%9c%8d%e5%8a%a1%e5%99%a8/#comments</comments>
		<pubDate>Sat, 24 Aug 2013 02:57:27 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Video]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1819</guid>
		<description><![CDATA[<p>本文基于Python后台搭建了一个视频服务器，集视频的上传，视频的格式转化，视频的播放于一体，后台基于Djan [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2013/08/%e6%90%ad%e5%bb%ba%e5%9f%ba%e4%ba%8epython%e7%9a%84%e8%a7%86%e9%a2%91%e6%9c%8d%e5%8a%a1%e5%99%a8/">搭建基于Python的视频服务器</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>本文基于Python后台搭建了一个视频服务器，集视频的上传，视频的格式转化，视频的播放于一体，后台基于Django框架和Amazon S3的存储，视频格式的转换基于Encoding.com的在线服务，消息队列基于RabbitMQ，视频上传和转换处理完毕后，采用浏览器的Html5播放，采用了Video.js。</p>
<p>Stickyworld's consultation web app has supported video for a long time but it's been hosted via a YouTube embed. When we started building the new version of the web app we wanted to take control of the video content and also free our users from YouTube's terms of service.<span id="more-1819"></span></p>
<p>I personally had worked on projects with clients in the past which did video transcoding and it was never something easy to achieve. It takes a lot to accept every video, audio and container format under the sun and output them into various video formats that the web knows and loves.</p>
<p>With that in mind we decided the conversion process would be handled by<a href="http://www.encoding.com/r?id=22129">Encoding.com</a>. They let you encode your first Gigabyte of video with them for free and then have a tiered pricing system there after.</p>
<p>Throughout the development of the code below I would upload a two-second, 178KB video to test that everything was working. When the exceptions stopped being raised, I tested larger and more exotic files.</p>
<h2>Stage 1: The User uploads a video</h2>
<p>At the moment the new codebase just has a quick-and-dirty HTML5-based uploading mechanism. This is the CoffeeScript for uploading from the client to the server:</p>
<pre>$scope.upload_slide = (upload_slide_form) -&gt;
    file = document.getElementById("slide_file").files[0]
    reader = new FileReader()
    reader.readAsDataURL file
    reader.onload = (event) -&gt;
      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) -&gt;
          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 = -&gt;
      console.log "onloadstart"
    reader.onprogress = (event) -&gt;
      console.log "onprogress", event.total, event.loaded, (event.loaded / event.total) * 100
    reader.onabort = -&gt;
      console.log "onabort"
    reader.onerror = -&gt;
      console.log "onerror"
    reader.onloadend = (event) -&gt;
      console.log "onloadend", event</pre>
<p>It would be nice to loop through <tt>("slide_file").files</tt> and upload each file individually instead of just the first file. This is somewhere we'll be addressing soon.</p>
<h2>Stage 2: Validate and upload to S3</h2>
<p>On the backend we're running Django and RabbitMQ. The key modules we're using are:</p>
<pre>$ pip install 'Django&gt;=1.5.2' 'django-celery&gt;=3.0.21' \
    'django-storages&gt;=1.1.8' 'lxml&gt;=3.2.3' 'python-magic&gt;=0.4.3'</pre>
<p>I created two models: <tt>SlideUploadQueue</tt> to store references to all the uploads and <tt>SlideVideoMedia</tt> to store all the references to the processed videos.</p>
<pre>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 &lt;SlideUploadQueue&gt;'
        return '&lt;SlideUploadQueue&gt; %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 &lt;SlideVideoMedia&gt;'
        return '&lt;SlideVideoMedia&gt; %d' % self.id</pre>
<p>Our models use a <tt>filename_sanitiser</tt> method in each <tt>models.FileField</tt> field to automatically adjust filenames into a <tt>&lt;model&gt;/&lt;uuid4&gt;.&lt;extention&gt;</tt>format. This sanitises each filename and makes sure they're unique. To add to that, we used signed URLs that expire so we can control who is served our content and for how long.</p>
<pre>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)</pre>
<p>A file uploaded as <tt>testing.mov</tt> would turn into <tt>https://our-bucket.s3.amazonaws.com/slideuploadqueue/3fe27193-e87f-4244-9aa2-66409f70ebd3.mov</tt> and would be uploaded by Django Storages.</p>
<p>In our backend endpoint where video is uploaded to from the browser we validate the uploaded content using <a href="https://github.com/ahupp/python-magic">Magic</a>. This detects what kind of file it is based on it's contents:</p>
<pre>@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)</pre>
<p>So if <tt>description</tt> matches something like <tt>MPEG v4 system</tt> or <tt>Apple QuickTime movie</tt> then we know it's suitable for transcoding. If it isn't something like the above, we can flag it up with the user.</p>
<p>Next, we'll save the video into a our <tt>SlideUploadQueue</tt> model and send a job off to RabbitMQ. Because we're using Django Storages, it'll be uploaded to Amazon S3 automatically.</p>
<pre>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)</pre>
<h2>Stage 3: Send the video to a 3rd party</h2>
<p>RabbitMQ will take over and handle the <tt>task.delay(slide_upload)</tt> call.</p>
<p>Here all we're doing is sending <a href="http://www.encoding.com/r?id=22129">Encoding.com</a> a URL of our video and instructions on what output formats we want. They'll give us a job number in return which we'll use to check up on the progress on the transcoding at a later point.</p>
<pre>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</pre>
<p><a href="http://www.encoding.com/r?id=22129">Encoding.com</a> recommended some <a href="https://github.com/djworth/python-encoding/blob/master/encoding/__init__.py">less-than-ideal python wrappers</a> for communicating with their service. I added a few fixes into the module but there is still work to be done to get it to a state I'm happy with. Below is what this wrapper currently looks like in our codebase:</p>
<pre>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</pre>
<p>Left on the todo list include HTTPS-only transmission with strict validation of <a href="http://www.encoding.com/r?id=22129">Encoding.com</a>'s SSL certificate and to write some unit tests (tickets, tickets and more tickets).</p>
<h2>Stage 4: Download all new video formats</h2>
<p>We have a periodic job running every 15 seconds via RabbitMQ which is checking up on the progress of the video transcoding:</p>
<pre>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</pre>
<p>We'll go through the response from <a href="http://www.encoding.com/r?id=22129">Encoding.com</a> validating each piece of the response as we go along:</p>
<pre>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) &gt; 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</pre>
<p>At this point we've asserted everything is as it should be a we can save each of the videos:</p>
<pre>media = SlideVideoMedia()
media.format = format_lookup[output]
media.converted_file.save('blah.%s' % file_ext, ContentFile(content))
media.save()</pre>
<h2>Stage 5: Video via HTML5</h2>
<p>On our frontend we've created a page with an HTML5 video element. We're using <a href="http://www.videojs.com/">video.js</a> to display the video in the best-supported format for each browser.</p>
<pre>➫ 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</pre>
<p>In our <tt>index.jade</tt> file we include it's dependencies:</p>
<pre>!!! 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')</pre>
<p>In a Angular.js/JADE-based template we've included a <tt>&lt;video&gt;</tt> tag and it's <tt>&lt;source&gt;</tt> children tags. There is also a <tt>poster</tt> element that will show a static image of the video we've transcoded from the first few moments of the video.</p>
<pre>#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}}")</pre>
<p>This will print out every video format we've converted into, each as it's own <tt>&lt;source&gt;</tt> tag. <a href="http://www.videojs.com/">Video.js</a> will decide which of them to play based on the browser the user is using.</p>
<p>We still have a lot of work to do around fallback support, building unit tests and improving the robustness of our <a href="http://www.encoding.com/r?id=22129">Encoding.com</a> service wrapper. If this sort of work interests you please do <a href="http://uk.linkedin.com/in/marklitwintschik">get in touch</a>.</p>
<p>文章节选：<a href="http://techblog.stickyworld.com/video-with-python.html" target="_blank">http://techblog.stickyworld.com/video-with-python.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2013/08/%e6%90%ad%e5%bb%ba%e5%9f%ba%e4%ba%8epython%e7%9a%84%e8%a7%86%e9%a2%91%e6%9c%8d%e5%8a%a1%e5%99%a8/">搭建基于Python的视频服务器</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2013/08/%e6%90%ad%e5%bb%ba%e5%9f%ba%e4%ba%8epython%e7%9a%84%e8%a7%86%e9%a2%91%e6%9c%8d%e5%8a%a1%e5%99%a8/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>编译 faplayer 播放器</title>
		<link>http://blog.zhourunsheng.com/2012/05/%e7%bc%96%e8%af%91-faplayer-%e6%92%ad%e6%94%be%e5%99%a8/</link>
		<comments>http://blog.zhourunsheng.com/2012/05/%e7%bc%96%e8%af%91-faplayer-%e6%92%ad%e6%94%be%e5%99%a8/#comments</comments>
		<pubDate>Sat, 26 May 2012 05:17:31 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[faplayer]]></category>
		<category><![CDATA[Video]]></category>
		<category><![CDATA[视频]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1582</guid>
		<description><![CDATA[<p>最近在做一个有关视频播放的项目，涉及到的问题就是视频解码，目前最优秀的两套解码库就是ffmpeg和flv，几乎 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/05/%e7%bc%96%e8%af%91-faplayer-%e6%92%ad%e6%94%be%e5%99%a8/">编译 faplayer 播放器</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>最近在做一个有关视频播放的项目，涉及到的问题就是视频解码，目前最优秀的两套解码库就是ffmpeg和flv，几乎现在能见到的所有视频格式都能解码，本文编译的faplayer就是基于flv的解码库。</p>
<p>编译成功的程序可以正常播放优酷的flv格式，截图如下：</p>
<p><img src="http://blog.zhourunsheng.com/wp-content/uploads/2012/05/faplayer_pause.png" alt="faplayer" width="426" height="229" /><br />
<span id="more-1582"></span></p>
<h2>环境配置</h2>
<ul>
<li>Android SDk 参见 <a href="http://developer.android.com/sdk/index.html">http://developer.android.com/sdk/index.html</a></li>
<li>Android NDk 参见 <a href="http://developer.android.com/sdk/ndk/index.html">http://developer.android.com/sdk/ndk/index.html</a></li>
<li>环境变量配置如下（~/.bashrc）：</li>
</ul>
<pre>   export ANDROID_SDK=/home/careychow/android/sdk/android-sdk-linux
   # export ANDROID_NDK=/home/careychow/android/ndk/android-ndk-r8
   export ANDROID_NDK=/home/careychow/android/ndk/android-ndk-r5c
   export PATH=$PATH:$ANDROID_SDK/tools:$ANDROID_SDK/platform-toos:$ANDROID_NDK</pre>
<p><strong>注</strong>： faplayer 利用 android-ndk-r5c 编译，用 android-ndk-r8 编译会出现错误</p>
<ul>
<li>安装ruby环境，推荐安装ruby1.9.2</li>
</ul>
<pre>sudo apt-get install ruby</pre>
<h2>手机CPU类型检测</h2>
<p>我自己的三星s5830i cpu 信息如下</p>
<pre>    C:\Users\Administrator&gt;adb shell cat /proc/cpuinfo
    Processor       : ARMv6-compatible processor rev 5 (v6l)
    BogoMIPS        : 832.90
    Features        : swp half thumb fastmult vfp edsp java
    CPU implementer : 0x41
    CPU architecture: 6TEJ
    CPU variant     : 0x1
    CPU part        : 0xb36
    CPU revision    : 5

    Hardware        : BCM21553 ThunderbirdEDN31 platform
    Revision        : 0000
    Serial          : 0000000000000000</pre>
<h2>编译 faplayer</h2>
<ul>
<li>下载代码
<pre> git clone https://github.com/tewilove/faplayer.git</pre>
</li>
<li>配置 local.properties 指向android sdk的安装路径，我的配置是：
<pre> sdk.dir=/home/careychow/android/sdk/android-sdk-linux</pre>
</li>
<li>配置编译参数并编译代码，具体的配置需要参照上面的cpu信息，如果类型不匹配，编译出来的apk是无法正常安装的，会出现 <strong>Failure [INSTALL_FAILED_CPU_ABI_INCOMPATIBLE]</strong>错误，我的手机不支持neon，所以采用第二个配置参数 ABI=armeabi，可正常安装和执行
<pre>Build examples:
./build.sh # &lt;-- this will build for cortex-a8 with neon by default.
./build.sh ABI=armeabi # &lt;-- this will build for arm1136j-s with softfp.
./build.sh ABI=armeabi-v7a FPU=vfpv3-d16 TUNE=cortex-a9 # &lt;-- Xoom</pre>
</li>
<li>生成 APK
<pre> ant debug</pre>
<p>命令后会在bin文件夹下面生成 faplayer-debug.apk<br />
<strong>注</strong>：如果提示build.xml文件过期，则可用如下命令修复</p>
<pre> android update project -p .</pre>
</li>
</ul>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/05/%e7%bc%96%e8%af%91-faplayer-%e6%92%ad%e6%94%be%e5%99%a8/">编译 faplayer 播放器</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/05/%e7%bc%96%e8%af%91-faplayer-%e6%92%ad%e6%94%be%e5%99%a8/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
