1、未完成,待补充,完善后会上传完整代码(包含ffmpeg).目标平台暂定ios,完善后会完美跨平台。
2、实用价值跟遇到的困难不成正比,研究价值更大。
3、我们需要一个通用的,可嵌入到游戏内部的视频播放控件。 现有的解决方案都是android和iOS各自实用系统控件进行封装。好处是实现简单,一般情况下稳定,并且解码效率高。 缺点是无法与游戏真正契合在一起,毕竟是作为只能悬浮在游戏之上,并且iOS下面的MPPlayer还有一些恶心的问题,比如视频方向和大小莫名其妙的改变了。
另外,一个通用的播放控件可以完美的跨平台。比如现在的wp8版本暂时还无法播放视频,而且可以想见,如果使用的是native c++的系统结构,那么一定没有系统视频控件。
------------------------------------正文-----------------------------------
1、首先我们要做的是下载ffmpeg的代码 (git clone git://source.ffmpeg.org/ffmpeg.git ffmpeg) ,在windows下编译ffmpeg是相当麻烦的一件事(原因是微软的vs死活不支持c99,而ffmpeg死活要用c99),这个最后再研究。iOS下面最简单,所以先研究iOS平台。 另外要注意,使用越新版本的ffmpeg,出问题的可能性越大。所以直接clone上面的地址不见得是一件正确的事情,直接下载一个稳定版本最好。
2、下载最新的gas-preprocessor并拷贝到/usr/bin目录下,这个是unix下用来预处理汇编文件的脚本,mac下没有,需要到libav的网站去下载(http://git.libav.org/?p=gas-preprocessor.git),如果版本与ffmpeg不一致的话,可能会在编译汇编文件的时候出现编译错误
unknown register alias 'TCOS_D0_HEAD'
ps:小说明一下libav,这个是ffmpeg的一个fork分支,起因是领导者对如何维护ffmpeg项目的分歧。代码非常相似,并且ffmpeg会经常从libav merge代码。不过无所谓一个要优于另外一个,否则就不会依然存在两个项目了。
3、在命令行下面切换到ffmpeg代码目录,运行configure
编译armv7版本的静态库
./configure --prefix=armv7 --disable-ffmpeg --disable-ffplay --disable-ffprobe --disable-ffserver --enable-avresample --enable-cross-compile --sysroot="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.1.sdk" --target-os=darwin --cc="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/gcc" --extra-cflags="-arch armv7 -mfpu=neon -miphoneos-version-min=5.1" --extra-ldflags="-arch armv7 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.1.sdk -miphoneos-version-min=4.3" --arch=arm --cpu=cortex-a9 --enable-pic
待执行完毕后,运行 make; make install,如果权限不做就加上sudo。
注意几个参数,--prefix指定了make install的时候拷贝头文件和静态库的目标路径。 --sysroot指定了SDK版本,这个基本上一个xcode只有一个,要指定对,否则无法运行gcc。 --arch --cpu 指定了当前cpu架构。 另外一些参数决定了要编译的内容。如果全部编译的话静态库有100+mb,很多编码器和解码器我们是用不到的,可以直接干掉。同理,如果我们想要在模拟器下运行的话,还需要i386版本的静态库。
./configure --prefix=i386 --disable-ffmpeg --disable-ffplay --disable-ffprobe --disable-ffserver --enable-avresample --enable-cross-compile --sysroot="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator6.1.sdk" --target-os=darwin --cc="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin/gcc" --extra-cflags="-arch i386" --extra-ldflags="-arch i386 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator6.1.sdk" --arch=i386 --cpu=i386 --enable-pic --disable-asm
4、当生成完毕两个版本的静态库后,我们需要使用lipo来合并两个版本,制作一个通用库。
lipo -output universal/lib/libavcodec.a -create -arch armv7 armv7/lib/libavcodec.a -arch i386 i386/lib/libavcodec.a
lipo -output universal/lib/libavdevice.a -create -arch armv7 armv7/lib/libavdevice.a -arch i386 i386/lib/libavdevice.a
lipo -output universal/lib/libavfilter.a -create -arch armv7 armv7/lib/libavfilter.a -arch i386 i386/lib/libavfilter.a
lipo -output universal/lib/libavformat.a -create -arch armv7 armv7/lib/libavformat.a -arch i386 i386/lib/libavformat.a
lipo -output universal/lib/libavresample.a -create -arch armv7 armv7/lib/libavresample.a -arch i386 i386/lib/libavresample.a
lipo -output universal/lib/libavutil.a -create -arch armv7 armv7/lib/libavutil.a -arch i386 i386/lib/libavutil.a
lipo -output universal/lib/libswresample.a -create -arch armv7 armv7/lib/libswresample.a -arch i386 i386/lib/libswresample.a
lipo -output universal/lib/libswscale.a -create -arch armv7 armv7/lib/libswscale.a -arch i386 i386/lib/libswscale.a
如果熟悉shell脚本的话,可以把上面的这些操作写成一个shell脚本,这样就方便很多了。 这里直接贴出这些步骤,一个图方便,二是明了我们真正需要操作什么。
5、生成通用静态库后,就可以直接把这些库加入到xcode项目中,设置好头文件依赖。 这里需要注意的是,包含ffmpeg头文件时需要用extern "C" {} 来括起来,否则就是一大堆链接错误
6、写一个CCVideoLayer来用cocos2d-x播放视频
头文件
#pragma once
#include "cocos2d.h"
#ifdef __APPLE__
#include <tr1/functional>
namespace std {
namespace tr1 {}
using namespace tr1;
using tr1::function;
}
//using namespace std::tr1;
#else
#include <functional>
#endif
struct AVFormatContext;
struct AVCodecContext;
struct AVFrame;
struct AVPicture;
struct SwsContext;
NS_CC_BEGIN
class CCVideoLayer : public CCSprite
{
public:
static CCVideoLayer* create(const char* path, int width, int height);
CCVideoLayer();
virtual ~CCVideoLayer();
bool init(const char* path, int width, int height);
void play(void);
void stop(void);
void pause(void);
void seek(double sec);
void draw(void);
void update(float dt);
virtual bool ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent);
virtual void ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent);
virtual void ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent);
virtual void ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent);
void setEnableTouchEnd(bool enable); // 是否可以点击关闭视频
void setVideoEndCallback(std::function<void()> func); // 关闭视频回调
private:
unsigned int m_width;
unsigned int m_height;
AVFormatContext *pFormatCtx;
AVCodecContext *pCodecCtx;
AVFrame *pFrame;
AVPicture* picture;
int videoStream; // 视频流
int audioStream; // 音频流
SwsContext *img_convert_ctx;
std::string m_filePath;
double m_frameRate; // 帧率
double m_elapsed; // 用于帧率控制
bool m_enableTouchEnd;
std::function<void()> m_videoEndCallback;
};
NS_CC_END
实现文件
//
// CCVideoLayer.cpp
// libquickcocos2dx
//
// Created by langresser on 13-11-7.
// Copyright (c) 2013年 qeeplay.com. All rights reserved.
//
#include "CCVideoLayer.h"
#include "SimpleAudioEngine.h"
#define kEnableFFMPEG 0
#if kEnableFFMPEG
extern "C" {
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
}
#endif
using namespace CocosDenshion;
NS_CC_BEGIN
CCVideoLayer* CCVideoLayer::create(const char* path, int width, int height)
{
CCVideoLayer* video = new CCVideoLayer();
if (video) {
video->init(path, width, height);
}
return video;
}
CCVideoLayer::CCVideoLayer()
{
#if kEnableFFMPEG
pFormatCtx = NULL;
pCodecCtx = NULL;
pFrame = NULL;
picture = NULL;
img_convert_ctx = NULL;
#endif
m_frameRate = 1 / 30.0;
m_elapsed = 0;
m_enableTouchEnd = false;
}
CCVideoLayer::~CCVideoLayer()
{
#if kEnableFFMPEG
sws_freeContext(img_convert_ctx);
// Free RGB picture
avpicture_free(picture);
delete picture;
// Free the YUV frame
av_free(pFrame);
// Close the codec
if (pCodecCtx) avcodec_close(pCodecCtx);
if (pFormatCtx) {
avformat_close_input(&pFormatCtx);
}
#endif
}
bool CCVideoLayer::init(const char* path, int width, int height)
{
#if kEnableFFMPEG
AVCodec *pCodec;
m_width = width;
m_height = height;
// Register all formats and codecs
av_register_all();
m_filePath = CCFileUtils::sharedFileUtils()->fullPathForFilename(path);
if(avformat_open_input(&pFormatCtx, m_filePath.c_str(), NULL, NULL) != 0) {
return false;
}
// 获取流信息
if(avformat_find_stream_info(pFormatCtx, NULL) < 0) {
return false;
}
// 查找视频流和音频流,由于不是做播放器,只取第一个流就可以了。视频格式为游戏服务
videoStream = -1;
audioStream = -1;
for(int i=0; i<pFormatCtx->nb_streams; i++) {
//if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO)
if(videoStream == -1 && pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
videoStream=i;
}
if (audioStream == -1 && pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) {
audioStream = i;
}
if (videoStream != -1 && audioStream != -1) {
break;
}
}
// 没有视频流,无法播放
if(videoStream == -1) {
return false;
}
// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
// 获取视频帧率
AVRational rational = pFormatCtx->streams[videoStream]->r_frame_rate;
m_frameRate = 1.0 * rational.den / rational.num;
m_frameRate = 1.0 / 31;
// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
return false;
}
if(avcodec_open2(pCodecCtx, pCodec, NULL)) {
return false;
}
// Allocate video frame
pFrame=avcodec_alloc_frame();
// scale
sws_freeContext(img_convert_ctx);
// 用于渲染的一帧图片数据。注意其中的data是一个指针数组,我们取视频流用于渲染(一般是第0个流)
picture = new AVPicture;
avpicture_alloc(picture, PIX_FMT_RGB24, m_width, m_height);
// 用于缩放视频到实际需求大小
static int sws_flags = SWS_FAST_BILINEAR;
img_convert_ctx = sws_getContext(pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt,
m_width,
m_height,
PIX_FMT_RGB24,
sws_flags, NULL, NULL, NULL);
// 渲染的纹理
CCTexture2D *texture = new CCTexture2D();
texture->initWithData(picture->data[videoStream], picture->linesize[videoStream]*m_height, kCCTexture2DPixelFormat_RGB888, m_width, m_height, CCSize(m_width, m_height));
initWithTexture(texture);
this->setContentSize(CCSize(m_width, m_height));
SimpleAudioEngine::sharedEngine()->preloadBackgroundMusic(m_filePath.c_str());
#endif
return true;
}
void CCVideoLayer::play()
{
std::string path = m_filePath.substr(0, m_filePath.rfind('.')) + ".m4a";
SimpleAudioEngine::sharedEngine()->playBackgroundMusic(path.c_str());
m_elapsed = 0;
seek(0);
this->schedule(schedule_selector(CCVideoLayer::update), m_frameRate);
}
void CCVideoLayer::stop(void)
{
this->unscheduleAllSelectors();
SimpleAudioEngine::sharedEngine()->stopBackgroundMusic();
}
void CCVideoLayer::pause(void)
{
SimpleAudioEngine::sharedEngine()->pauseBackgroundMusic();
}
void CCVideoLayer::seek(double sec)
{
#if kEnableFFMPEG
AVRational timeBase = pFormatCtx->streams[videoStream]->time_base;
int64_t targetFrame = (int64_t)((double)timeBase.den / timeBase.num * sec);
avformat_seek_file(pFormatCtx, videoStream, targetFrame, targetFrame, targetFrame, AVSEEK_FLAG_FRAME);
avcodec_flush_buffers(pCodecCtx);
#endif
}
void CCVideoLayer::update(float dt)
{
#if kEnableFFMPEG
m_elapsed += dt;
// if (m_elapsed < m_frameRate) {
// return;
// }
m_elapsed = 0;
AVPacket packet;
int frameFinished=0;
while(!frameFinished && av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
}
// Free the packet that was allocated by av_read_frame
av_free_packet(&packet);
}
sws_scale (img_convert_ctx, pFrame->data, pFrame->linesize,
0, pCodecCtx->height,
picture->data, picture->linesize);
if (frameFinished == 0) {
this->stop();
if (m_videoEndCallback) {
m_videoEndCallback();
}
this->removeFromParentAndCleanup(true);
}
#endif
}
void CCVideoLayer::draw(void)
{
#if kEnableFFMPEG
CC_PROFILER_START_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");
CCAssert(!m_pobBatchNode, "If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");
CC_NODE_DRAW_SETUP();
ccGLBlendFunc( m_sBlendFunc.src, m_sBlendFunc.dst );
if (m_pobTexture != NULL)
{
ccGLBindTexture2D( m_pobTexture->getName() );
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_width, m_height, 0, GL_RGB, GL_UNSIGNED_BYTE,picture->data[videoStream]);
//glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_width, m_height, GL_RGB, GL_UNSIGNED_BYTE,picture->data[videoStream]);
}
else
{
ccGLBindTexture2D(0);
}
//
// Attributes
//
ccGLEnableVertexAttribs( kCCVertexAttribFlag_PosColorTex );
#define kQuadSize sizeof(m_sQuad.bl)
long offset = (long)&m_sQuad;
// vertex
int diff = offsetof( ccV3F_C4B_T2F, vertices);
glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE, kQuadSize, (void*) (offset + diff));
// texCoods
diff = offsetof( ccV3F_C4B_T2F, texCoords);
glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, kQuadSize, (void*)(offset + diff));
// color
diff = offsetof( ccV3F_C4B_T2F, colors);
glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (void*)(offset + diff));
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
CHECK_GL_ERROR_DEBUG();
CC_INCREMENT_GL_DRAWS(1);
CC_PROFILER_STOP_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");
#endif
}
bool CCVideoLayer::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent)
{
return true;
}
void CCVideoLayer::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent)
{
}
void CCVideoLayer::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent)
{
if (m_enableTouchEnd) {
this->stop();
if (m_videoEndCallback) {
m_videoEndCallback();
}
this->removeFromParentAndCleanup(true);
}
}
void CCVideoLayer::ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent)
{
}
void CCVideoLayer::setEnableTouchEnd(bool enable)
{
m_enableTouchEnd = enable;
if (enable) {
setEventMode(kEventModeNormal);
}
}
void CCVideoLayer::setVideoEndCallback(std::function<void(void)> func)
{
m_videoEndCallback = func;
}
NS_CC_END
相关说明:
上面的这个CCVideoLayer只是初步实现了视频播放功能(ios下测试完毕),后续还有很多要完善的地方。不过最近新的项目要开启了,估计短时间内不会继续研究这个了。
值得完善的地方:
1、使用glSubTexImage来提交纹理更新,而不是使用glTexImage提交纹理,这个有助于效率提升(具体情况待测试)
2、基本的帧率控制在update函数里面
3、没有实现声音播放。声音解码和播放需要再研究一下。(我希望fmodex可以实现buffer声音的播放,要不然每个平台实现一套声音播放引擎太麻烦了)
4、个人对这个还是比较满意的,可以向CCSprite一样随意的蹂躏,也可以直接放在lua里面作为一个对象来处理。不用担心一些系统控件的实现细节。效率也可以接受。