Cocos2d-x 视频播放(VideoPlayer组件与Cocos Creator支持)全解析
【摘要】 1. 引言在现代游戏开发中,视频播放已成为不可或缺的功能,无论是作为开场动画、剧情过场、教程演示还是动态UI元素,视频都能显著提升游戏的视觉表现力和叙事能力。Cocos2d-x作为跨平台游戏引擎,在不同版本中对视频播放的支持经历了从基础到完善的演进过程。特别是随着Cocos Creator的兴起,视频播放功能得到了更强大的支持和更友好的开发体验。本文将全面探讨Cocos2d-x环境下的视频播...
1. 引言
2. 技术背景
2.1 Cocos2d-x视频播放发展历程
-
Cocos2d-x v3.x时代:基础VideoPlayer组件,依赖平台原生API -
Cocos2d-x v4.x时代:增强的平台适配,支持更多视频格式 -
Cocos Creator 1.x-2.x:引入节点化VideoPlayer组件,支持编辑器集成 -
Cocos Creator 3.x:统一的渲染管线,更好的性能和跨平台一致性
2.2 底层技术架构
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.3 Cocos Creator视频组件演进
-
Creator 1.x:基础VideoPlayer组件,简单的播放控制 -
Creator 2.x:增强的事件系统,支持更多自定义属性 -
Creator 3.x:基于TypeScript的强类型组件,更好的性能监控
3. 应用使用场景
3.1 典型应用场景分类
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3.2 场景复杂度分析
-
简单场景:本地视频文件播放,基础控制(播放/暂停/停止) -
中级场景:带交互的视频播放,事件回调,多格式支持 -
复杂场景:网络视频流,动态切换,性能优化,跨平台适配 -
企业级场景:DRM保护,广告集成,数据分析,A/B测试
4. 核心原理与流程图
4.1 视频播放架构图
graph TD
A[Cocos2d-x/Creator] --> B[VideoPlayer Component]
B --> C[Platform Adapter Layer]
C --> D[Windows: DirectShow]
C --> E[macOS: AVFoundation]
C --> F[iOS: AVPlayer]
C --> G[Android: MediaPlayer/ExoPlayer]
C --> H[Web: HTML5 Video]
B --> I[Texture Renderer]
I --> J[OpenGL/Vulkan/Metal]
B --> K[Event System]
K --> L[Playback Events]
K --> M[User Interaction Events]
C --> N[Media Decoder]
N --> O[Audio Output]
N --> P[Video Frame Output]
4.2 视频播放工作流程
sequenceDiagram
participant User as 用户/游戏逻辑
participant VP as VideoPlayer组件
participant PL as 平台播放器
participant TX as 纹理系统
participant RD as 渲染器
User->>VP: play(url)
VP->>PL: initializePlayer(url)
PL->>PL: loadMedia(url)
PL-->>VP: onPrepared()
VP->>TX: createVideoTexture()
VP->>PL: startPlayback()
loop 播放循环
PL->>PL: decodeNextFrame()
PL->>TX: updateTexture(frameData)
TX->>RD: bindTexture()
RD->>Screen: renderFrame()
end
PL-->>VP: onComplete()
VP->>User: dispatchEvent(COMPLETED)
4.3 工作原理详解
-
组件初始化:创建平台特定的播放器实例,配置渲染目标 -
媒体加载:异步加载视频文件,解析元数据(时长、分辨率、编码格式) -
纹理绑定:将视频帧数据映射到GPU纹理,实现高效渲染 -
播放控制:通过统一接口控制播放、暂停、 seek等操作 -
事件分发:将平台原生事件转换为Cocos事件系统可识别的格式 -
资源回收:播放完成后释放纹理和播放器资源
5. 环境准备
5.1 开发环境配置
# 创建Cocos2d-x项目(v4.0+)
cocos new VideoPlayerDemo -p com.yourcompany.videoplayer -l cpp -d ./projects
# 创建Cocos Creator项目(3.8+)
cocos new VideoPlayerCreator -p com.yourcompany.videoplayercreator -l ts -d ./creator_projects
# 目录结构规划
VideoPlayerDemo/
├── Resources/
│ ├── videos/ # 视频资源
│ │ ├── intro/ # 开场视频
│ │ ├── cutscenes/ # 过场视频
│ │ └── tutorials/ # 教程视频
│ ├── images/ # 图片资源
│ └── fonts/ # 字体文件
├── Classes/ # C++源代码
│ ├── video/ # 视频播放模块
│ │ ├── VideoPlayer.h
│ │ ├── VideoPlayer.cpp
│ │ ├── PlatformVideoPlayer.h
│ │ └── VideoEvents.h
│ ├── scenes/ # 场景类
│ └── utils/ # 工具类
└── proj.* # 各平台工程文件
VideoPlayerCreator/
├── assets/
│ ├── video/ # 视频资源
│ ├── scripts/ # TypeScript脚本
│ ├── scenes/ # 场景文件
│ └── settings/ # 项目设置
└── project.json # 项目配置
5.2 平台特定依赖
// app/build.gradle 添加依赖
dependencies {
implementation 'com.google.android.exoplayer:exoplayer:2.18.7'
implementation 'com.google.android.exoplayer:extension-rtmp:2.18.7'
}
// AndroidManifest.xml 添加权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Info.plist 添加配置 -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>voip</string>
</array>
5.3 项目配置
# 添加视频播放模块
file(GLOB_RECURSE VIDEO_MODULE_FILES
Classes/video/*.h
Classes/video/*.cpp
)
list(APPEND GAME_SRC ${VIDEO_MODULE_FILES})
# 平台特定编译选项
if(ANDROID)
add_definitions(-DANDROID_VIDEO_PLAYER_ENABLED)
find_library(LOG_LIB log)
target_link_libraries(${APP_NAME} ${LOG_LIB})
elseif(IOS)
add_definitions(-DIOS_VIDEO_PLAYER_ENABLED)
find_library(AVFOUNDATION_LIB AVFoundation)
find_library(MEDIAPLAYER_LIB MediaPlayer)
target_link_libraries(${APP_NAME} ${AVFOUNDATION_LIB} ${MEDIAPLAYER_LIB})
endif()
6. 详细代码实现
6.1 视频事件系统
#ifndef __VIDEO_EVENTS_H__
#define __VIDEO_EVENTS_H__
#include "cocos2d.h"
#include <string>
NS_CC_BEGIN
// 视频播放事件类型
enum class VideoEventType {
PLAYING, // 开始播放
PAUSED, // 暂停
STOPPED, // 停止
COMPLETED, // 播放完成
BUFFERING_START, // 开始缓冲
BUFFERING_END, // 缓冲结束
ERROR_OCCURRED, // 发生错误
PREPARED, // 准备完成
SEEK_COMPLETED, // Seek完成
METADATA_RECEIVED, // 元数据接收
VOLUME_CHANGED, // 音量变化
BITRATE_CHANGED // 码率变化(自适应流)
};
// 视频事件数据
struct VideoEvent {
VideoEventType type;
std::string videoUrl;
double currentTime;
double duration;
int errorCode;
std::string errorMessage;
int width;
int height;
float volume;
VideoEvent(VideoEventType t = VideoEventType::ERROR_OCCURRED)
: type(t)
, currentTime(0.0)
, duration(0.0)
, errorCode(0)
, width(0)
, height(0)
, volume(1.0f) {}
};
// 视频事件监听器接口
class VideoEventListener {
public:
virtual ~VideoEventListener() = default;
virtual void onVideoEvent(const VideoEvent& event) = 0;
};
// 视频事件分发器
class VideoEventDispatcher {
public:
static VideoEventDispatcher* getInstance();
static void destroyInstance();
void addListener(VideoEventListener* listener);
void removeListener(VideoEventListener* listener);
void dispatchEvent(const VideoEvent& event);
private:
VideoEventDispatcher() = default;
~VideoEventDispatcher() = default;
static VideoEventDispatcher* _instance;
std::vector<VideoEventListener*> _listeners;
mutable std::mutex _mutex;
};
NS_CC_END
#endif // __VIDEO_EVENTS_H__
#include "VideoEvents.h"
USING_NS_CC;
VideoEventDispatcher* VideoEventDispatcher::_instance = nullptr;
VideoEventDispatcher* VideoEventDispatcher::getInstance() {
if (!_instance) {
_instance = new (std::nothrow) VideoEventDispatcher();
}
return _instance;
}
void VideoEventDispatcher::destroyInstance() {
CC_SAFE_DELETE(_instance);
}
void VideoEventDispatcher::addListener(VideoEventListener* listener) {
std::lock_guard<std::mutex> lock(_mutex);
if (listener && std::find(_listeners.begin(), _listeners.end(), listener) == _listeners.end()) {
_listeners.push_back(listener);
}
}
void VideoEventDispatcher::removeListener(VideoEventListener* listener) {
std::lock_guard<std::mutex> lock(_mutex);
auto it = std::find(_listeners.begin(), _listeners.end(), listener);
if (it != _listeners.end()) {
_listeners.erase(it);
}
}
void VideoEventDispatcher::dispatchEvent(const VideoEvent& event) {
std::lock_guard<std::mutex> lock(_mutex);
for (auto* listener : _listeners) {
if (listener) {
listener->onVideoEvent(event);
}
}
CCLOG("VideoEvent: Dispatched event type %d for video %s",
(int)event.type, event.videoUrl.c_str());
}
6.2 平台视频播放器抽象
#ifndef __PLATFORM_VIDEO_PLAYER_H__
#define __PLATFORM_VIDEO_PLAYER_H__
#include "VideoEvents.h"
#include "cocos2d.h"
#include <string>
#include <functional>
NS_CC_BEGIN
class PlatformVideoPlayer : public VideoEventListener {
public:
virtual ~PlatformVideoPlayer() = default;
// 播放控制
virtual bool initialize(const std::string& url) = 0;
virtual bool play() = 0;
virtual bool pause() = 0;
virtual bool stop() = 0;
virtual bool seekTo(double seconds) = 0;
// 属性设置
virtual void setVolume(float volume) = 0;
virtual float getVolume() const = 0;
virtual void setLooping(bool looping) = 0;
virtual bool isLooping() const = 0;
virtual void setMute(bool mute) = 0;
virtual bool isMute() const = 0;
// 状态查询
virtual bool isPlaying() const = 0;
virtual bool isPaused() const = 0;
virtual bool isPrepared() const = 0;
virtual double getCurrentTime() const = 0;
virtual double getDuration() const = 0;
virtual std::string getVideoUrl() const = 0;
// 纹理相关
virtual void* getVideoTexture() = 0;
virtual cocos2d::Size getVideoSize() const = 0;
// 事件回调
virtual void setEventCallback(const std::function<void(const VideoEvent&)>& callback) = 0;
protected:
virtual void onVideoEvent(const VideoEvent& event) override {
if (_eventCallback) {
_eventCallback(event);
}
}
std::function<void(const VideoEvent&)> _eventCallback;
};
NS_CC_END
#endif // __PLATFORM_VIDEO_PLAYER_H__
6.3 主视频播放器组件
#ifndef __VIDEO_PLAYER_H__
#define __VIDEO_PLAYER_H__
#include "PlatformVideoPlayer.h"
#include "cocos2d.h"
#include <memory>
NS_CC_BEGIN
class VideoPlayer : public cocos2d::Node, public VideoEventListener {
public:
enum class ScaleMode {
NONE, // 不缩放
FILL, // 填充整个区域
ASPECT_FILL, // 按比例填充(可能裁剪)
ASPECT_FIT // 按比例适应(可能有黑边)
};
static VideoPlayer* create();
static VideoPlayer* create(const std::string& videoUrl);
virtual bool init() override;
virtual bool initWithUrl(const std::string& videoUrl);
virtual void onEnter() override;
virtual void onExit() override;
virtual void update(float delta) override;
// 播放控制
bool play();
bool pause();
bool stop();
bool seekTo(double seconds);
void restart();
// 资源加载
bool loadVideo(const std::string& url);
void unloadVideo();
// 属性设置
void setScaleMode(ScaleMode mode);
ScaleMode getScaleMode() const { return _scaleMode; }
void setVolume(float volume);
float getVolume() const;
void setLooping(bool looping);
bool isLooping() const;
void setMute(bool mute);
bool isMute() const;
void setKeepAspectRatio(bool keep);
bool isKeepAspectRatio() const { return _keepAspectRatio; }
// 外观控制
void setVisible(bool visible) override;
void setOpacity(GLubyte opacity) override;
void setPosition(const cocos2d::Vec2& pos) override;
void setContentSize(const cocos2d::Size& size) override;
// 状态查询
bool isPlaying() const;
bool isPaused() const;
bool isPrepared() const;
double getCurrentTime() const;
double getDuration() const;
std::string getVideoUrl() const;
cocos2d::Size getVideoSize() const;
// 事件监听
void setVideoEventListener(VideoEventListener* listener);
// 渲染控制
void setRenderTexture(cocos2d::Texture2D* texture);
cocos2d::Texture2D* getRenderTexture() const { return _renderTexture; }
private:
VideoPlayer();
virtual ~VideoPlayer();
bool createRenderTexture();
void updateVideoTexture();
void updateDisplay();
void calculateVideoRect();
void dispatchEventToListeners(const VideoEvent& event);
// 平台播放器
std::unique_ptr<PlatformVideoPlayer> _platformPlayer;
// 渲染相关
cocos2d::Sprite* _videoSprite;
cocos2d::Texture2D* _renderTexture;
cocos2d::RenderTexture* _customRenderTexture;
// 属性
ScaleMode _scaleMode;
bool _keepAspectRatio;
bool _visible;
GLubyte _opacity;
cocos2d::Vec2 _position;
cocos2d::Size _contentSize;
// 事件监听
VideoEventListener* _eventListener;
// 内部状态
bool _prepared;
bool _firstFrameRendered;
cocos2d::Rect _videoRect;
// 平台特定实现
void createPlatformPlayer();
};
NS_CC_END
#endif // __VIDEO_PLAYER_H__
#include "VideoPlayer.h"
#include "PlatformVideoPlayer.h"
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "video/AndroidVideoPlayer.h"
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
#include "video/iOSVideoPlayer.h"
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)
#include "video/Win32VideoPlayer.h"
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_MAC)
#include "video/MacVideoPlayer.h"
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_HTML5)
#include "video/WebVideoPlayer.h"
#endif
USING_NS_CC;
VideoPlayer::VideoPlayer()
: _platformPlayer(nullptr)
, _videoSprite(nullptr)
, _renderTexture(nullptr)
, _customRenderTexture(nullptr)
, _scaleMode(ScaleMode::ASPECT_FIT)
, _keepAspectRatio(true)
, _visible(true)
, _opacity(255)
, _prepared(false)
, _firstFrameRendered(false)
, _eventListener(nullptr) {
createPlatformPlayer();
}
VideoPlayer::~VideoPlayer() {
if (_platformPlayer) {
_platformPlayer->stop();
}
VideoEventDispatcher::getInstance()->removeListener(this);
CC_SAFE_RELEASE_NULL(_renderTexture);
CC_SAFE_RELEASE_NULL(_customRenderTexture);
}
VideoPlayer* VideoPlayer::create() {
auto player = new (std::nothrow) VideoPlayer();
if (player && player->init()) {
player->autorelease();
return player;
}
CC_SAFE_DELETE(player);
return nullptr;
}
VideoPlayer* VideoPlayer::create(const std::string& videoUrl) {
auto player = new (std::nothrow) VideoPlayer();
if (player && player->initWithUrl(videoUrl)) {
player->autorelease();
return player;
}
CC_SAFE_DELETE(player);
return nullptr;
}
bool VideoPlayer::init() {
if (!Node::init()) {
return false;
}
// 创建精灵用于显示视频
_videoSprite = Sprite::create();
_videoSprite->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
this->addChild(_videoSprite);
// 注册事件监听
VideoEventDispatcher::getInstance()->addListener(this);
// 创建渲染纹理
if (!createRenderTexture()) {
CCLOG("VideoPlayer: Failed to create render texture");
return false;
}
// 设置默认内容大小
this->setContentSize(Size(1280, 720));
return true;
}
bool VideoPlayer::initWithUrl(const std::string& videoUrl) {
if (!init()) {
return false;
}
return loadVideo(videoUrl);
}
void VideoPlayer::onEnter() {
Node::onEnter();
this->scheduleUpdate();
}
void VideoPlayer::onExit() {
this->unscheduleUpdate();
Node::onExit();
}
void VideoPlayer::update(float delta) {
if (_prepared && _platformPlayer) {
updateVideoTexture();
}
}
bool VideoPlayer::createRenderTexture() {
// 创建离屏渲染纹理
_customRenderTexture = RenderTexture::create(1920, 1080, Texture2D::PixelFormat::RGBA8888);
if (!_customRenderTexture) {
return false;
}
_customRenderTexture->retain();
return true;
}
void VideoPlayer::createPlatformPlayer() {
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
_platformPlayer = std::make_unique<AndroidVideoPlayer>();
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
_platformPlayer = std::make_unique<iOSVideoPlayer>();
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)
_platformPlayer = std::make_unique<Win32VideoPlayer>();
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_MAC)
_platformPlayer = std::make_unique<MacVideoPlayer>();
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_HTML5)
_platformPlayer = std::make_unique<WebVideoPlayer>();
#else
// 默认实现(空播放器)
class DummyVideoPlayer : public PlatformVideoPlayer {
public:
virtual bool initialize(const std::string& url) override { return false; }
virtual bool play() override { return false; }
virtual bool pause() override { return false; }
virtual bool stop() override { return false; }
virtual bool seekTo(double seconds) override { return false; }
virtual void setVolume(float volume) override {}
virtual float getVolume() const override { return 0.0f; }
virtual void setLooping(bool looping) override {}
virtual bool isLooping() const override { return false; }
virtual void setMute(bool mute) override {}
virtual bool isMute() const override { return true; }
virtual bool isPlaying() const override { return false; }
virtual bool isPaused() const override { return false; }
virtual bool isPrepared() const override { return false; }
virtual double getCurrentTime() const override { return 0.0; }
virtual double getDuration() const override { return 0.0; }
virtual std::string getVideoUrl() const override { return ""; }
virtual void* getVideoTexture() override { return nullptr; }
virtual cocos2d::Size getVideoSize() const override { return Size(0, 0); }
virtual void setEventCallback(const std::function<void(const VideoEvent&)>& callback) override {}
};
_platformPlayer = std::make_unique<DummyVideoPlayer>();
#endif
if (_platformPlayer) {
_platformPlayer->setEventCallback(
[this](const VideoEvent& event) {
this->onVideoEvent(event);
}
);
}
}
bool VideoPlayer::loadVideo(const std::string& url) {
if (!_platformPlayer) {
CCLOG("VideoPlayer: Platform player not available");
return false;
}
// 停止当前播放
stop();
// 重置状态
_prepared = false;
_firstFrameRendered = false;
// 初始化平台播放器
if (!_platformPlayer->initialize(url)) {
CCLOG("VideoPlayer: Failed to initialize platform player for URL: %s", url.c_str());
return false;
}
CCLOG("VideoPlayer: Successfully loaded video: %s", url.c_str());
return true;
}
void VideoPlayer::unloadVideo() {
stop();
if (_platformPlayer) {
_platformPlayer.reset();
createPlatformPlayer();
}
_prepared = false;
_firstFrameRendered = false;
}
bool VideoPlayer::play() {
if (!_platformPlayer || !_prepared) {
CCLOG("VideoPlayer: Cannot play - player not prepared");
return false;
}
bool success = _platformPlayer->play();
if (success) {
CCLOG("VideoPlayer: Playback started");
} else {
CCLOG("VideoPlayer: Failed to start playback");
}
return success;
}
bool VideoPlayer::pause() {
if (!_platformPlayer) {
return false;
}
bool success = _platformPlayer->pause();
if (success) {
CCLOG("VideoPlayer: Playback paused");
}
return success;
}
bool VideoPlayer::stop() {
if (!_platformPlayer) {
return false;
}
bool success = _platformPlayer->stop();
if (success) {
_prepared = false;
_firstFrameRendered = false;
CCLOG("VideoPlayer: Playback stopped");
}
return success;
}
bool VideoPlayer::seekTo(double seconds) {
if (!_platformPlayer || !_prepared) {
return false;
}
return _platformPlayer->seekTo(seconds);
}
void VideoPlayer::restart() {
if (!_platformPlayer || !_prepared) {
return;
}
seekTo(0.0);
play();
}
void VideoPlayer::setScaleMode(ScaleMode mode) {
_scaleMode = mode;
calculateVideoRect();
updateDisplay();
}
void VideoPlayer::setVolume(float volume) {
if (_platformPlayer) {
_platformPlayer->setVolume(cocos2d::clampf(volume, 0.0f, 1.0f));
}
}
float VideoPlayer::getVolume() const {
return _platformPlayer ? _platformPlayer->getVolume() : 0.0f;
}
void VideoPlayer::setLooping(bool looping) {
if (_platformPlayer) {
_platformPlayer->setLooping(looping);
}
}
bool VideoPlayer::isLooping() const {
return _platformPlayer ? _platformPlayer->isLooping() : false;
}
void VideoPlayer::setMute(bool mute) {
if (_platformPlayer) {
_platformPlayer->setMute(mute);
}
}
bool VideoPlayer::isMute() const {
return _platformPlayer ? _platformPlayer->isMute() : true;
}
void VideoPlayer::setKeepAspectRatio(bool keep) {
_keepAspectRatio = keep;
calculateVideoRect();
updateDisplay();
}
void VideoPlayer::setVisible(bool visible) {
Node::setVisible(visible);
_visible = visible;
if (_videoSprite) {
_videoSprite->setVisible(visible);
}
}
void VideoPlayer::setOpacity(GLubyte opacity) {
Node::setOpacity(opacity);
_opacity = opacity;
if (_videoSprite) {
_videoSprite->setOpacity(opacity);
}
}
void VideoPlayer::setPosition(const cocos2d::Vec2& pos) {
Node::setPosition(pos);
_position = pos;
if (_videoSprite) {
_videoSprite->setPosition(Vec2::ZERO);
}
}
void VideoPlayer::setContentSize(const cocos2d::Size& size) {
Node::setContentSize(size);
_contentSize = size;
calculateVideoRect();
updateDisplay();
}
bool VideoPlayer::isPlaying() const {
return _platformPlayer ? _platformPlayer->isPlaying() : false;
}
bool VideoPlayer::isPaused() const {
return _platformPlayer ? _platformPlayer->isPaused() : false;
}
bool VideoPlayer::isPrepared() const {
return _platformPlayer ? _platformPlayer->isPrepared() : false;
}
double VideoPlayer::getCurrentTime() const {
return _platformPlayer ? _platformPlayer->getCurrentTime() : 0.0;
}
double VideoPlayer::getDuration() const {
return _platformPlayer ? _platformPlayer->getDuration() : 0.0;
}
std::string VideoPlayer::getVideoUrl() const {
return _platformPlayer ? _platformPlayer->getVideoUrl() : "";
}
cocos2d::Size VideoPlayer::getVideoSize() const {
return _platformPlayer ? _platformPlayer->getVideoSize() : Size(0, 0);
}
void VideoPlayer::setVideoEventListener(VideoEventListener* listener) {
_eventListener = listener;
}
void VideoPlayer::setRenderTexture(cocos2d::Texture2D* texture) {
if (_renderTexture) {
_renderTexture->release();
}
_renderTexture = texture;
if (_renderTexture) {
_renderTexture->retain();
if (_videoSprite) {
_videoSprite->setTexture(_renderTexture);
}
}
}
void VideoPlayer::updateVideoTexture() {
if (!_platformPlayer || !_prepared) {
return;
}
// 获取平台视频纹理
void* platformTexture = _platformPlayer->getVideoTexture();
if (!platformTexture) {
return;
}
// 在实际实现中,这里需要将平台纹理转换为Cocos2d-x纹理
// 由于平台差异,具体实现在各平台子类中
// 标记第一帧已渲染
if (!_firstFrameRendered) {
_firstFrameRendered = true;
CCLOG("VideoPlayer: First frame rendered");
}
}
void VideoPlayer::updateDisplay() {
if (_videoSprite && _renderTexture) {
_videoSprite->setTextureRect(_videoRect);
_videoSprite->setPosition(_videoRect.origin);
}
}
void VideoPlayer::calculateVideoRect() {
if (!_videoSprite) {
return;
}
cocos2d::Size videoSize = getVideoSize();
if (videoSize.width <= 0 || videoSize.height <= 0) {
return;
}
cocos2d::Size containerSize = _contentSize;
cocos2d::Rect rect;
switch (_scaleMode) {
case ScaleMode::NONE:
rect = Rect(0, 0, videoSize.width, videoSize.height);
break;
case ScaleMode::FILL:
rect = Rect(0, 0, containerSize.width, containerSize.height);
break;
case ScaleMode::ASPECT_FILL: {
float scaleX = containerSize.width / videoSize.width;
float scaleY = containerSize.height / videoSize.height;
float scale = MAX(scaleX, scaleY);
float width = videoSize.width * scale;
float height = videoSize.height * scale;
float x = (containerSize.width - width) / 2.0f;
float y = (containerSize.height - height) / 2.0f;
rect = Rect(x, y, width, height);
break;
}
case ScaleMode::ASPECT_FIT:
default: {
float scaleX = containerSize.width / videoSize.width;
float scaleY = containerSize.height / videoSize.height;
float scale = MIN(scaleX, scaleY);
float width = videoSize.width * scale;
float height = videoSize.height * scale;
float x = (containerSize.width - width) / 2.0f;
float y = (containerSize.height - height) / 2.0f;
rect = Rect(x, y, width, height);
break;
}
}
_videoRect = rect;
updateDisplay();
}
void VideoPlayer::onVideoEvent(const VideoEvent& event) {
// 更新内部状态
switch (event.type) {
case VideoEventType::PREPARED:
_prepared = true;
calculateVideoRect();
break;
case VideoEventType::COMPLETED:
_prepared = false;
break;
case VideoEventType::ERROR_OCCURRED:
CCLOG("VideoPlayer: Error occurred - Code: %d, Message: %s",
event.errorCode, event.errorMessage.c_str());
break;
default:
break;
}
// 转发事件给外部监听器
dispatchEventToListeners(event);
}
void VideoPlayer::dispatchEventToListeners(const VideoEvent& event) {
if (_eventListener) {
_eventListener->onVideoEvent(event);
}
// 也发送给全局事件分发器
VideoEventDispatcher::getInstance()->dispatchEvent(event);
}
6.4 Android平台实现(示例)
#ifndef __ANDROID_VIDEO_PLAYER_H__
#define __ANDROID_VIDEO_PLAYER_H__
#include "PlatformVideoPlayer.h"
#include <jni.h>
#include "platform/android/jni/JniHelper.h"
NS_CC_BEGIN
class AndroidVideoPlayer : public PlatformVideoPlayer {
public:
AndroidVideoPlayer();
virtual ~AndroidVideoPlayer();
virtual bool initialize(const std::string& url) override;
virtual bool play() override;
virtual bool pause() override;
virtual bool stop() override;
virtual bool seekTo(double seconds) override;
virtual void setVolume(float volume) override;
virtual float getVolume() const override;
virtual void setLooping(bool looping) override;
virtual bool isLooping() const override;
virtual void setMute(bool mute) override;
virtual bool isMute() const override;
virtual bool isPlaying() const override;
virtual bool isPaused() const override;
virtual bool isPrepared() const override;
virtual double getCurrentTime() const override;
virtual double getDuration() const override;
virtual std::string getVideoUrl() const override;
virtual void* getVideoTexture() override;
virtual cocos2d::Size getVideoSize() const override;
virtual void setEventCallback(const std::function<void(const VideoEvent&)>& callback) override;
private:
bool callJavaMethod(const char* methodName, const char* paramCode, ...);
void sendEventToCpp(int eventType, const std::string& data = "");
jobject _javaPlayerObj;
jclass _javaPlayerClass;
std::string _currentUrl;
float _volume;
bool _looping;
bool _muted;
bool _preparedFlag;
};
NS_CC_END
#endif // __ANDROID_VIDEO_PLAYER_H__
#include "AndroidVideoPlayer.h"
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include <android/log.h>
#define LOG_TAG "AndroidVideoPlayer"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
USING_NS_CC;
static const char* CLASS_NAME = "org/cocos2dx/cpp/VideoPlayerHelper";
AndroidVideoPlayer::AndroidVideoPlayer()
: _javaPlayerObj(nullptr)
, _javaPlayerClass(nullptr)
, _volume(1.0f)
, _looping(false)
, _muted(false)
, _preparedFlag(false) {
}
AndroidVideoPlayer::~AndroidVideoPlayer() {
if (_javaPlayerObj) {
JniHelper::getEnv()->DeleteGlobalRef(_javaPlayerObj);
}
if (_javaPlayerClass) {
JniHelper::getEnv()->DeleteGlobalRef(_javaPlayerClass);
}
}
bool AndroidVideoPlayer::initialize(const std::string& url) {
JniMethodInfo methodInfo;
// 获取Java类
if (!JniHelper::getStaticMethodInfo(methodInfo, CLASS_NAME, "createPlayer", "()Ljava/lang/Object;")) {
LOGE("Failed to get createPlayer method");
return false;
}
_javaPlayerObj = methodInfo.env->CallStaticObjectMethod(methodInfo.classID, methodInfo.methodID);
methodInfo.env->DeleteLocalRef(methodInfo.classID);
if (!_javaPlayerObj) {
LOGE("Failed to create Java video player");
return false;
}
_javaPlayerObj = methodInfo.env->NewGlobalRef(_javaPlayerObj);
// 获取Java类引用
_javaPlayerClass = methodInfo.env->GetObjectClass(_javaPlayerObj);
_javaPlayerClass = (jclass)methodInfo.env->NewGlobalRef(_javaPlayerClass);
_currentUrl = url;
_preparedFlag = false;
// 初始化播放器
if (!callJavaMethod("initialize", "(Ljava/lang/String;)Z", url.c_str())) {
LOGE("Failed to initialize video player with URL: %s", url.c_str());
return false;
}
LOGD("AndroidVideoPlayer initialized with URL: %s", url.c_str());
return true;
}
bool AndroidVideoPlayer::play() {
return callJavaMethod("play", "()Z");
}
bool AndroidVideoPlayer::pause() {
return callJavaMethod("pause", "()Z");
}
bool AndroidVideoPlayer::stop() {
bool result = callJavaMethod("stop", "()Z");
_preparedFlag = false;
return result;
}
bool AndroidVideoPlayer::seekTo(double seconds) {
return callJavaMethod("seekTo", "(D)Z", seconds);
}
void AndroidVideoPlayer::setVolume(float volume) {
_volume = cocos2d::clampf(volume, 0.0f, 1.0f);
callJavaMethod("setVolume", "(F)V", _volume);
}
float AndroidVideoPlayer::getVolume() const {
return _volume;
}
void AndroidVideoPlayer::setLooping(bool looping) {
_looping = looping;
callJavaMethod("setLooping", "(Z)V", looping);
}
bool AndroidVideoPlayer::isLooping() const {
return _looping;
}
void AndroidVideoPlayer::setMute(bool mute) {
_muted = mute;
callJavaMethod("setMute", "(Z)V", mute);
}
bool AndroidVideoPlayer::isMute() const {
return _muted;
}
bool AndroidVideoPlayer::isPlaying() const {
jboolean result = JNI_FALSE;
callJavaMethod("isPlaying", "()Z", &result);
return result == JNI_TRUE;
}
bool AndroidVideoPlayer::isPaused() const {
jboolean result = JNI_FALSE;
callJavaMethod("isPaused", "()Z", &result);
return result == JNI_TRUE;
}
bool AndroidVideoPlayer::isPrepared() const {
return _preparedFlag;
}
double AndroidVideoPlayer::getCurrentTime() const {
jdouble result = 0.0;
callJavaMethod("getCurrentTime", "()D", &result);
return result;
}
double AndroidVideoPlayer::getDuration() const {
jdouble result = 0.0;
callJavaMethod("getDuration", "()D", &result);
return result;
}
std::string AndroidVideoPlayer::getVideoUrl() const {
return _currentUrl;
}
void* AndroidVideoPlayer::getVideoTexture() {
// Android平台需要通过SurfaceTexture获取视频纹理
// 这里返回nullptr,实际实现需要处理纹理共享
return nullptr;
}
cocos2d::Size AndroidVideoPlayer::getVideoSize() const {
jint width = 0, height = 0;
if (callJavaMethod("getVideoWidth", "()I", &width) &&
callJavaMethod("getVideoHeight", "()I", &height)) {
return cocos2d::Size(width, height);
}
return cocos2d::Size(0, 0);
}
void AndroidVideoPlayer::setEventCallback(const std::function<void(const VideoEvent&)>& callback) {
PlatformVideoPlayer::setEventCallback(callback);
// 设置Java端的回调
JniMethodInfo methodInfo;
if (JniHelper::getStaticMethodInfo(methodInfo, CLASS_NAME, "setEventCallback", "(Z)V")) {
methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID, JNI_TRUE);
methodInfo.env->DeleteLocalRef(methodInfo.classID);
}
}
bool AndroidVideoPlayer::callJavaMethod(const char* methodName, const char* paramCode, ...) {
if (!_javaPlayerObj || !_javaPlayerClass) {
return false;
}
JNIEnv* env = JniHelper::getEnv();
if (!env) {
return false;
}
jmethodID method = env->GetMethodID(_javaPlayerClass, methodName, paramCode);
if (!method) {
LOGE("Method %s not found with signature %s", methodName, paramCode);
return false;
}
va_list args;
va_start(args, paramCode);
jboolean result = JNI_FALSE;
if (strcmp(paramCode, "()Z") == 0) {
result = env->CallBooleanMethodV(_javaPlayerObj, method, args);
} else if (strcmp(paramCode, "()V") == 0) {
env->CallVoidMethodV(_javaPlayerObj, method, args);
result = JNI_TRUE;
} else if (strcmp(paramCode, "(Ljava/lang/String;)Z") == 0) {
jstring jstr = env->NewStringUTF(va_arg(args, const char*));
result = env->CallBooleanMethodV(_javaPlayerObj, method, args);
env->DeleteLocalRef(jstr);
} else if (strcmp(paramCode, "(F)V") == 0) {
env->CallVoidMethodV(_javaPlayerObj, method, args);
result = JNI_TRUE;
} else if (strcmp(paramCode, "(Z)V") == 0) {
env->CallVoidMethodV(_javaPlayerObj, method, args);
result = JNI_TRUE;
} else if (strcmp(paramCode, "()D") == 0) {
jdouble ret = env->CallDoubleMethodV(_javaPlayerObj, method, args);
// 处理返回值
result = JNI_TRUE;
} else if (strcmp(paramCode, "()I") == 0) {
jint ret = env->CallIntMethodV(_javaPlayerObj, method, args);
// 处理返回值
result = JNI_TRUE;
} else {
LOGE("Unsupported parameter code: %s", paramCode);
va_end(args);
return false;
}
va_end(args);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
LOGE("Java exception in method %s", methodName);
return false;
}
return result == JNI_TRUE;
}
void AndroidVideoPlayer::sendEventToCpp(int eventType, const std::string& data) {
// 这个方法供Java端调用,通过JNI发送事件到C++
// 实际实现需要在Java类中注册native方法
}
#endif // CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID
6.5 场景集成示例
#ifndef __VIDEO_PLAYER_DEMO_SCENE_H__
#define __VIDEO_PLAYER_DEMO_SCENE_H__
#include "cocos2d.h"
#include "video/VideoPlayer.h"
NS_CC_BEGIN
class VideoPlayerDemoScene : public Scene, public VideoEventListener {
public:
static Scene* createScene();
virtual bool init() override;
CREATE_FUNC(VideoPlayerDemoScene);
private:
void createUI();
void onPlayButtonClicked(Ref* sender);
void onPauseButtonClicked(Ref* sender);
void onStopButtonClicked(Ref* sender);
void onSeekButtonClicked(Ref* sender);
void onVolumeUpClicked(Ref* sender);
void onVolumeDownClicked(Ref* sender);
void onVideoEvent(const VideoEvent& event);
void updateProgress(float dt);
Layer* _uiLayer;
VideoPlayer* _videoPlayer;
Label* _statusLabel;
Label* _progressLabel;
Slider* _progressSlider;
bool _userSeeking;
};
NS_CC_END
#endif // __VIDEO_PLAYER_DEMO_SCENE_H__
#include "VideoPlayerDemoScene.h"
#include "ui/CocosGUI.h"
USING_NS_CC;
using namespace ui;
Scene* VideoPlayerDemoScene::createScene() {
auto scene = VideoPlayerDemoScene::create();
return scene;
}
bool VideoPlayerDemoScene::init() {
if (!Scene::init()) {
return false;
}
auto visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
_uiLayer = Layer::create();
this->addChild(_uiLayer);
// 标题
auto title = Label::createWithTTF("Video Player Demo", "fonts/arial.ttf", 36);
title->setPosition(Vec2(origin.x + visibleSize.width / 2,
origin.y + visibleSize.height * 0.95));
title->setColor(Color3B::WHITE);
_uiLayer->addChild(title);
// 创建视频播放器
_videoPlayer = VideoPlayer::create("videos/intro/welcome.mp4");
if (_videoPlayer) {
_videoPlayer->setPosition(Vec2(origin.x + visibleSize.width / 2,
origin.y + visibleSize.height * 0.5));
_videoPlayer->setContentSize(Size(visibleSize.width * 0.8f, visibleSize.height * 0.6f));
_videoPlayer->setScaleMode(VideoPlayer::ScaleMode::ASPECT_FIT);
_videoPlayer->setVideoEventListener(this);
_uiLayer->addChild(_videoPlayer);
}
createUI();
// 注册事件监听
VideoEventDispatcher::getInstance()->addListener(this);
// 启动进度更新
this->schedule(CC_SCHEDULE_SELECTOR(VideoPlayerDemoScene::updateProgress), 0.5f);
return true;
}
void VideoPlayerDemoScene::createUI() {
auto visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
Menu* menu = Menu::create();
menu->setPosition(Vec2::ZERO);
_uiLayer->addChild(menu);
// 播放按钮
auto playBtn = MenuItemLabel::create(
Label::createWithTTF("Play", "fonts/arial.ttf", 24),
CC_CALLBACK_1(VideoPlayerDemoScene::onPlayButtonClicked, this)
);
playBtn->setPosition(Vec2(origin.x + visibleSize.width / 2 - 150,
origin.y + visibleSize.height * 0.15));
menu->addChild(playBtn);
// 暂停按钮
auto pauseBtn = MenuItemLabel::create(
Label::createWithTTF("Pause", "fonts/arial.ttf", 24),
CC_CALLBACK_1(VideoPlayerDemoScene::onPauseButtonClicked, this)
);
pauseBtn->setPosition(Vec2(origin.x + visibleSize.width / 2 - 50,
origin.y + visibleSize.height * 0.15));
menu->addChild(pauseBtn);
// 停止按钮
auto stopBtn = MenuItemLabel::create(
Label::createWithTTF("Stop", "fonts/arial.ttf", 24),
CC_CALLBACK_1(VideoPlayerDemoScene::onStopButtonClicked, this)
);
stopBtn->setPosition(Vec2(origin.x + visibleSize.width / 2 + 50,
origin.y + visibleSize.height * 0.15));
menu->addChild(stopBtn);
// Seek按钮
auto seekBtn = MenuItemLabel::create(
Label::createWithTTF("Seek to 10s", "fonts/arial.ttf", 24),
CC_CALLBACK_1(VideoPlayerDemoScene::onSeekButtonClicked, this)
);
seekBtn->setPosition(Vec2(origin.x + visibleSize.width / 2 + 150,
origin.y + visibleSize.height * 0.15));
menu->addChild(seekBtn);
// 音量控制按钮
auto volUpBtn = MenuItemLabel::create(
Label::createWithTTF("Vol +", "fonts/arial.ttf", 24),
CC_CALLBACK_1(VideoPlayerDemoScene::onVolumeUpClicked, this)
);
volUpBtn->setPosition(Vec2(origin.x + visibleSize.width / 2 - 100,
origin.y + visibleSize.height * 0.08));
menu->addChild(volUpBtn);
auto volDownBtn = MenuItemLabel::create(
Label::createWithTTF("Vol -", "fonts/arial.ttf", 24),
CC_CALLBACK_1(VideoPlayerDemoScene::onVolumeDownClicked, this)
);
volDownBtn->setPosition(Vec2(origin.x + visibleSize.width / 2 + 100,
origin.y + visibleSize.height * 0.08));
menu->addChild(volDownBtn);
// 状态标签
_statusLabel = Label::createWithTTF("Ready", "fonts/arial.ttf", 20);
_statusLabel->setPosition(Vec2(origin.x + visibleSize.width / 2,
origin.y + visibleSize.height * 0.25));
_statusLabel->setColor(Color3B::YELLOW);
_uiLayer->addChild(_statusLabel);
// 进度标签
_progressLabel = Label::createWithTTF("00:00 / 00:00", "fonts/arial.ttf", 20);
_progressLabel->setPosition(Vec2(origin.x + visibleSize.width / 2,
origin.y + visibleSize.height * 0.30));
_progressLabel->setColor(Color3B::GREEN);
_uiLayer->addChild(_progressLabel);
}
void VideoPlayerDemoScene::onPlayButtonClicked(Ref* sender) {
if (_videoPlayer) {
if (_videoPlayer->play()) {
_statusLabel->setString("Playing");
} else {
_statusLabel->setString("Play failed");
}
}
}
void VideoPlayerDemoScene::onPauseButtonClicked(Ref* sender) {
if (_videoPlayer) {
if (_videoPlayer->pause()) {
_statusLabel->setString("Paused");
} else {
_statusLabel->setString("Pause failed");
}
}
}
void VideoPlayerDemoScene::onStopButtonClicked(Ref* sender) {
if (_videoPlayer) {
_videoPlayer->stop();
_statusLabel->setString("Stopped");
}
}
void VideoPlayerDemoScene::onSeekButtonClicked(Ref* sender) {
if (_videoPlayer) {
if (_videoPlayer->seekTo(10.0)) {
_statusLabel->setString("Seeked to 10s");
} else {
_statusLabel->setString("Seek failed");
}
}
}
void VideoPlayerDemoScene::onVolumeUpClicked(Ref* sender) {
if (_videoPlayer) {
float currentVol = _videoPlayer->getVolume();
_videoPlayer->setVolume(currentVol + 0.1f);
_statusLabel->setString(StringUtils::format("Volume: %.1f", currentVol + 0.1f));
}
}
void VideoPlayerDemoScene::onVolumeDownClicked(Ref* sender) {
if (_videoPlayer) {
float currentVol = _videoPlayer->getVolume();
_videoPlayer->setVolume(currentVol - 0.1f);
_statusLabel->setString(StringUtils::format("Volume: %.1f", currentVol - 0.1f));
}
}
void VideoPlayerDemoScene::onVideoEvent(const VideoEvent& event) {
switch (event.type) {
case VideoEventType::PREPARED:
_statusLabel->setString("Prepared");
break;
case VideoEventType::PLAYING:
_statusLabel->setString("Playing");
break;
case VideoEventType::PAUSED:
_statusLabel->setString("Paused");
break;
case VideoEventType::COMPLETED:
_statusLabel->setString("Completed");
break;
case VideoEventType::ERROR_OCCURRED:
_statusLabel->setString(StringUtils::format("Error: %s", event.errorMessage.c_str()));
break;
default:
break;
}
CCLOG("Video Event: %d, Time: %.2f/%.2f",
(int)event.type, event.currentTime, event.duration);
}
void VideoPlayerDemoScene::updateProgress(float dt) {
if (_videoPlayer && _videoPlayer->isPlaying()) {
double current = _videoPlayer->getCurrentTime();
double duration = _videoPlayer->getDuration();
if (duration > 0) {
int currentSec = (int)current;
int durationSec = (int)duration;
int currentMin = currentSec / 60;
int currentSecRem = currentSec % 60;
int durationMin = durationSec / 60;
int durationSecRem = durationSec % 60;
char buffer[64];
snprintf(buffer, sizeof(buffer), "%02d:%02d / %02d:%02d",
currentMin, currentSecRem, durationMin, durationSecRem);
_progressLabel->setString(buffer);
}
}
}
6.6 AppDelegate集成
#include "AppDelegate.h"
#include "scenes/VideoPlayerDemoScene.h"
#include "video/VideoPlayer.h"
#include "video/VideoEventDispatcher.h"
// ... 其他代码不变 ...
bool AppDelegate::applicationDidFinishLaunching() {
// ... 之前的初始化代码 ...
// 初始化视频播放器系统
auto videoDispatcher = VideoEventDispatcher::getInstance();
// 创建并显示演示场景
auto scene = VideoPlayerDemoScene::createScene();
director->runWithScene(scene);
return true;
}
void AppDelegate::applicationDidEnterBackground() {
CCLOG("AppDelegate: Entering background");
// 暂停所有视频播放
// 在实际项目中,可以维护一个全局视频播放器列表
}
void AppDelegate::applicationWillEnterForeground() {
CCLOG("AppDelegate: Entering foreground");
// 恢复视频播放(根据用户设置)
}
7. 运行结果
7.1 预期效果
-
应用启动后显示视频播放器演示界面 -
视频播放器正确加载并显示欢迎视频 -
控制按钮(播放/暂停/停止/Seek/音量)正常工作 -
状态标签实时显示播放状态和错误信息 -
进度标签准确显示当前播放时间和总时长 -
视频切换时界面平滑过渡,无闪烁或卡顿 -
应用切入后台时视频自动暂停,返回前台时可恢复
7.2 控制台输出示例
VideoPlayer: Successfully loaded video: videos/intro/welcome.mp4
VideoEvent: Dispatched event type 3 for video videos/intro/welcome.mp4
VideoPlayer: Playback started
Video Event: 0, Time: 0.00/30.52
Video Event: 0, Time: 0.50/30.52
VideoPlayer: Playback paused
Video Event: 1, Time: 5.23/30.52
VideoPlayer: Playback resumed
VideoPlayer: Seeked to 10s
VideoPlayer: Playback completed
Video Event: 3, Time: 30.52/30.52
8. 测试步骤
8.1 功能测试
-
基础播放测试: // 测试用例 void testBasicPlayback() { auto player = VideoPlayer::create("test_video.mp4"); CC_ASSERT(player != nullptr); player->play(); CC_ASSERT(player->isPlaying()); std::this_thread::sleep_for(std::chrono::seconds(1)); player->pause(); CC_ASSERT(player->isPaused()); player->stop(); CC_ASSERT(!player->isPlaying() && !player->isPaused()); } -
Seek功能测试: -
测试Seek到不同时间点(开始、中间、结束附近) -
验证Seek后播放位置准确性
-
-
音量控制测试: -
测试音量增减功能 -
验证静音/取消静音功能
-
8.2 性能测试
-
内存占用测试: -
监控播放高清视频时的内存使用情况 -
测试长时间播放的内存稳定性
-
-
CPU占用测试: -
测量视频解码和渲染的CPU使用率 -
测试多视频同时播放的性能表现
-
-
发热测试: -
长时间播放视频监测设备温度变化 -
验证散热策略有效性
-
8.3 跨平台测试
-
格式兼容性测试: -
在各平台测试MP4、MOV、WebM等格式的播放兼容性 -
验证编解码器支持情况
-
-
网络视频测试: -
测试HTTP/HTTPS视频流的播放稳定性 -
验证网络中断和恢复的处理
-
8.4 自动化测试脚本
#!/bin/bash
# 视频播放自动化测试脚本
echo "开始视频播放测试..."
# 构建测试版本
./build_test.sh
# 安装并启动应用
adb install -r build/android/bin/VideoPlayerDemo-debug.apk
adb shell am start -n com.yourcompany.videoplayer/.AppActivity
sleep 5
# 测试视频播放
echo "测试视频播放功能..."
adb shell input tap 400 900 # 点击Play按钮
sleep 3
adb shell input tap 500 900 # 点击Pause按钮
sleep 1
adb shell input tap 600 900 # 点击Stop按钮
sleep 1
# 检查日志中的视频事件
logcat -d | grep "VideoPlayer" > video_test_log.txt
if grep -q "Playback completed" video_test_log.txt; then
echo "视频播放测试通过!"
else
echo "测试失败:视频播放异常!"
exit 1
fi
# 清理
rm video_test_log.txt
9. 部署场景
9.1 平台特定配置
-
Java辅助类:创建 VideoPlayerHelper.java
package org.cocos2dx.cpp;
import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
public class VideoPlayerHelper {
private static MediaPlayer mediaPlayer;
private static VideoPlayerHelper instance;
public static Object createPlayer() {
if (instance == null) {
instance = new VideoPlayerHelper();
}
return instance;
}
public boolean initialize(String url) {
try {
if (mediaPlayer != null) {
mediaPlayer.release();
}
mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(url);
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(mp -> {
// 通知C++准备完成
nativeOnVideoEvent(3, 0, mp.getDuration(), 0, 0, "");
});
mediaPlayer.setOnCompletionListener(mp -> {
nativeOnVideoEvent(4, mp.getCurrentPosition(), mp.getDuration(), 0, 0, "");
});
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
nativeOnVideoEvent(7, mp.getCurrentPosition(), mp.getDuration(), what, extra, "MediaPlayer error");
return true;
});
return true;
} catch (IOException e) {
e.printStackTrace();
nativeOnVideoEvent(7, 0, 0, -1, 0, e.getMessage());
return false;
}
}
public boolean play() {
if (mediaPlayer != null) {
mediaPlayer.start();
nativeOnVideoEvent(0, mediaPlayer.getCurrentPosition(), mediaPlayer.getDuration(), 0, 0, "");
return true;
}
return false;
}
public boolean pause() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.pause();
nativeOnVideoEvent(1, mediaPlayer.getCurrentPosition(), mediaPlayer.getDuration(), 0, 0, "");
return true;
}
return false;
}
public boolean stop() {
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.reset();
return true;
}
return false;
}
public boolean seekTo(double seconds) {
if (mediaPlayer != null) {
mediaPlayer.seekTo((int)(seconds * 1000));
return true;
}
return false;
}
public void setVolume(float volume) {
if (mediaPlayer != null) {
mediaPlayer.setVolume(volume, volume);
}
}
public void setLooping(boolean looping) {
if (mediaPlayer != null) {
mediaPlayer.setLooping(looping);
}
}
public void setMute(boolean mute) {
if (mediaPlayer != null) {
if (mute) {
mediaPlayer.setVolume(0, 0);
} else {
mediaPlayer.setVolume(1, 1);
}
}
}
public boolean isPlaying() {
return mediaPlayer != null && mediaPlayer.isPlaying();
}
public boolean isPaused() {
return mediaPlayer != null && !mediaPlayer.isPlaying() && mediaPlayer.getCurrentPosition() > 0;
}
public double getCurrentTime() {
return mediaPlayer != null ? mediaPlayer.getCurrentPosition() / 1000.0 : 0;
}
public double getDuration() {
return mediaPlayer != null ? mediaPlayer.getDuration() / 1000.0 : 0;
}
public int getVideoWidth() {
return mediaPlayer != null ? mediaPlayer.getVideoWidth() : 0;
}
public int getVideoHeight() {
return mediaPlayer != null ? mediaPlayer.getVideoHeight() : 0;
}
// Native方法声明
private static native void nativeOnVideoEvent(int eventType, double currentTime,
double duration, int errorCode,
int extra, String message);
}
-
JNI注册:在 jni/Android.mk中注册native方法
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := cocos2dcpp_shared
LOCAL_MODULE_FILENAME := libcocos2dcpp
# ... 其他配置 ...
LOCAL_SRC_FILES := hellocpp/main.cpp \
../../Classes/video/VideoPlayer.cpp \
../../Classes/video/AndroidVideoPlayer.cpp \
../../Classes/video/VideoEvents.cpp
# ... 其他配置 ...
include $(BUILD_SHARED_LIBRARY)
-
Objective-C辅助类:创建 iOSVideoPlayer.mm
#import "iOSVideoPlayer.h"
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
@interface iOSVideoPlayerImpl : NSObject
@property (nonatomic, strong) AVPlayer* player;
@property (nonatomic, strong) AVPlayerItem* playerItem;
@property (nonatomic, strong) AVPlayerLayer* playerLayer;
@property (nonatomic, assign) BOOL isPrepared;
@property (nonatomic, copy) void (^eventCallback)(int, double, double, int, int, NSString*);
@end
@implementation iOSVideoPlayerImpl
- (instancetype)init {
self = [super init];
if (self) {
_isPrepared = NO;
}
return self;
}
- (BOOL)initializeWithURL:(NSString*)url {
NSURL* videoURL = [NSURL URLWithString:url];
if (!videoURL) {
return NO;
}
self.playerItem = [AVPlayerItem playerItemWithURL:videoURL];
self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
// 添加观察者
[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
[self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
// 播放完成通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:self.playerItem];
return YES;
}
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {
if ([keyPath isEqualToString:@"status"]) {
AVPlayerItemStatus status = self.playerItem.status;
if (status == AVPlayerItemStatusReadyToPlay) {
self.isPrepared = YES;
if (self.eventCallback) {
self.eventCallback(3, CMTimeGetSeconds(self.playerItem.currentTime),
CMTimeGetSeconds(self.playerItem.duration), 0, 0, @"");
}
} else if (status == AVPlayerItemStatusFailed) {
if (self.eventCallback) {
self.eventCallback(7, 0, 0, -1, 0, @"Player item failed");
}
}
}
}
- (void)playerItemDidReachEnd:(NSNotification*)notification {
if (self.eventCallback) {
self.eventCallback(4, CMTimeGetSeconds(self.playerItem.currentTime),
CMTimeGetSeconds(self.playerItem.duration), 0, 0, @"");
}
}
- (BOOL)play {
if (self.player && self.isPrepared) {
[self.player play];
if (self.eventCallback) {
self.eventCallback(0, CMTimeGetSeconds(self.playerItem.currentTime),
CMTimeGetSeconds(self.playerItem.duration), 0, 0, @"");
}
return YES;
}
return NO;
}
- (BOOL)pause {
if (self.player && self.player.rate > 0) {
[self.player pause];
if (self.eventCallback) {
self.eventCallback(1, CMTimeGetSeconds(self.playerItem.currentTime),
CMTimeGetSeconds(self.playerItem.duration), 0, 0, @"");
}
return YES;
}
return NO;
}
- (BOOL)stop {
if (self.player) {
[self.player pause];
[self.player seekToTime:kCMTimeZero];
self.isPrepared = NO;
return YES;
}
return NO;
}
- (BOOL)seekTo:(double)seconds {
if (self.player) {
CMTime time = CMTimeMakeWithSeconds(seconds, NSEC_PER_SEC);
[self.player seekToTime:time];
return YES;
}
return NO;
}
- (void)setVolume:(float)volume {
if (self.player) {
self.player.volume = volume;
}
}
- (float)getVolume {
return self.player ? self.player.volume : 0.0f;
}
- (void)setLooping:(BOOL)looping {
// AVPlayer doesn't support direct looping, would need custom implementation
}
- (BOOL)isLooping {
return NO;
}
- (void)setMute:(BOOL)mute {
if (self.player) {
self.player.muted = mute;
}
}
- (BOOL)isMute {
return self.player ? self.player.muted : YES;
}
- (BOOL)isPlaying {
return self.player ? self.player.rate > 0 : NO;
}
- (BOOL)isPaused {
return self.player ? self.player.rate == 0 && CMTimeCompare(self.playerItem.currentTime, kCMTimeZero) > 0 : NO;
}
- (BOOL)isPrepared {
return _isPrepared;
}
- (double)getCurrentTime {
return self.player ? CMTimeGetSeconds(self.playerItem.currentTime) : 0.0;
}
- (double)getDuration {
return self.player ? CMTimeGetSeconds(self.playerItem.duration) : 0.0;
}
- (int)getVideoWidth {
if (self.playerItem && self.playerItem.asset.tracks.count > 0) {
AVAssetTrack* videoTrack = [[self.playerItem.asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
return videoTrack.naturalSize.width;
}
return 0;
}
- (int)getVideoHeight {
if (self.playerItem && self.playerItem.asset.tracks.count > 0) {
AVAssetTrack* videoTrack = [[self.playerItem.asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
return videoTrack.naturalSize.height;
}
return 0;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (self.playerItem) {
[self.playerItem removeObserver:self forKeyPath:@"status"];
[self.playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
}
}
@end
// C++包装类实现
bool iOSVideoPlayer::initialize(const std::string& url) {
NSString* nsUrl = [NSString stringWithUTF8String:url.c_str()];
_impl = [[iOSVideoPlayerImpl alloc] init];
__block bool result = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
result = [_impl initializeWithURL:nsUrl];
});
if (result) {
__weak iOSVideoPlayerImpl* weakImpl = _impl;
_impl.eventCallback = ^(int eventType, double currentTime, double duration, int errorCode, int extra, NSString* message) {
// 转换到C++回调
VideoEvent event(static_cast<VideoEventType>(eventType));
event.currentTime = currentTime;
event.duration = duration;
event.errorCode = errorCode;
event.errorMessage = [message UTF8String];
if (_eventCallback) {
_eventCallback(event);
}
};
}
return result;
}
// ... 其他方法实现类似,通过_objcImpl调用对应的Objective-C方法 ...
9.2 部署优化策略
-
资源分级: // 根据设备性能选择视频质量 void selectVideoQualityBasedOnDevice() { auto player = VideoPlayer::create(); // 检测设备性能 float memoryGB = getDeviceMemoryGB(); int gpuLevel = getDeviceGPULevel(); std::string videoUrl; if (memoryGB >= 4.0f && gpuLevel >= 3) { videoUrl = "videos/high_quality/welcome_1080p.mp4"; } else if (memoryGB >= 2.0f) { videoUrl = "videos/medium_quality/welcome_720p.mp4"; } else { videoUrl = "videos/low_quality/welcome_480p.mp4"; } player->loadVideo(videoUrl); } -
预加载策略: // 预加载下一个可能播放的视频 void preloadNextVideo(const std::string& nextVideoUrl) { // 在实际项目中,可以使用隐藏的VideoPlayer实例进行预加载 auto preloadPlayer = VideoPlayer::create(nextVideoUrl); preloadPlayer->loadVideo(nextVideoUrl); // 但不要调用play(),只是预加载 }
10. 疑难解答
10.1 常见问题及解决方案
-
原因:SurfaceTexture未正确配置或EGL上下文不匹配 -
解决:确保在GL线程中创建和更新纹理,检查SurfaceTexture的updateTexImage调用时机
-
原因:AVPlayer默认遵循视频元数据中的方向信息 -
解决:设置 videoComposition或使用AVPlayerLayer的videoGravity属性控制显示方式
-
原因:浏览器策略要求用户交互后才能自动播放音频 -
解决:首次播放需要用户手势触发,或设置 muted属性实现静音自动播放
-
原因:平台播放器对象未正确释放或纹理资源未回收 -
解决:在析构函数中确保所有平台资源释放,使用内存分析工具定期检查
10.2 调试技巧
// 视频播放器调试模式
#define VIDEO_PLAYER_DEBUG 1
#if VIDEO_PLAYER_DEBUG
#define VIDEO_DEBUG_LOG(format, ...) \
CCLOG("[VIDEO_DEBUG] " format, ##__VA_ARGS__)
class VideoPlayerDebugger {
public:
static void enablePerformanceMonitoring(VideoPlayer* player) {
if (!player) return;
// 监控帧率和内存使用
Director::getInstance()->getScheduler()->schedule(
[player](float dt) {
auto stats = player->getVideoSize();
float memUsage = getTextureMemoryUsage();
VIDEO_DEBUG_LOG("Video Stats: %dx%d, Mem: %.2fMB",
(int)stats.width, (int)stats.height, memUsage);
}, player, 1.0f, false, "VideoPerfMonitor");
}
static void dumpVideoState(VideoPlayer* player) {
if (!player) return;
VIDEO_DEBUG_LOG("=== Video Player State ===");
VIDEO_DEBUG_LOG("Playing: %s", player->isPlaying() ? "Yes" : "No");
VIDEO_DEBUG_LOG("Paused: %s", player->isPaused() ? "Yes" : "No");
VIDEO_DEBUG_LOG("Prepared: %s", player->isPrepared() ? "Yes" : "No");
VIDEO_DEBUG_LOG("Current Time: %.2fs", player->getCurrentTime());
VIDEO_DEBUG_LOG("Duration: %.2fs", player->getDuration());
VIDEO_DEBUG_LOG("Volume: %.2f", player->getVolume());
}
};
#else
#define VIDEO_DEBUG_LOG(...)
#endif
11. 未来展望与技术趋势
11.1 技术发展趋势
-
AV1编码普及:新一代视频编码标准带来更高压缩率和更好质量 -
硬件加速解码:更广泛的GPU视频解码支持,降低CPU负载 -
VR/AR视频集成:360度视频和空间音频的无缝集成 -
AI超分辨率:实时提升视频分辨率的AI技术应用 -
自适应流媒体:基于网络状况的动态码率调整更加智能
11.2 新兴挑战
-
版权保护:DRM和数字水印技术的应用需求增加 -
隐私安全:视频内容分析和用户行为追踪的隐私问题 -
能耗优化:移动设备对视频播放功耗的严格要求 -
标准化缺失:跨平台视频播放API的统一化需求
12. 总结
-
跨平台架构设计:统一的VideoPlayer组件抽象,屏蔽平台差异 -
完整的事件系统:细粒度的播放状态监控和回调机制 -
灵活的渲染控制:多种缩放模式和纹理管理方式 -
平台特定优化:针对Android、iOS、Web等平台的深度适配 -
生产级代码质量:异常处理、内存管理和性能优化
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)