Cocos2d-x 视频播放(VideoPlayer组件与Cocos Creator支持)全解析

举报
William 发表于 2025/12/12 10:14:41 2025/12/12
【摘要】 1. 引言在现代游戏开发中,视频播放已成为不可或缺的功能,无论是作为开场动画、剧情过场、教程演示还是动态UI元素,视频都能显著提升游戏的视觉表现力和叙事能力。Cocos2d-x作为跨平台游戏引擎,在不同版本中对视频播放的支持经历了从基础到完善的演进过程。特别是随着Cocos Creator的兴起,视频播放功能得到了更强大的支持和更友好的开发体验。本文将全面探讨Cocos2d-x环境下的视频播...


1. 引言

在现代游戏开发中,视频播放已成为不可或缺的功能,无论是作为开场动画、剧情过场、教程演示还是动态UI元素,视频都能显著提升游戏的视觉表现力和叙事能力。Cocos2d-x作为跨平台游戏引擎,在不同版本中对视频播放的支持经历了从基础到完善的演进过程。特别是随着Cocos Creator的兴起,视频播放功能得到了更强大的支持和更友好的开发体验。
本文将全面探讨Cocos2d-x环境下的视频播放解决方案,涵盖原生Cocos2d-x的VideoPlayer组件实现、Cocos Creator的深度集成、跨平台适配策略以及实际项目中的最佳实践。我们将从技术原理出发,提供完整的代码实现和详细的部署指南,帮助开发者构建稳定、高效的视频播放系统。

2. 技术背景

2.1 Cocos2d-x视频播放发展历程

  1. Cocos2d-x v3.x时代:基础VideoPlayer组件,依赖平台原生API
  2. Cocos2d-x v4.x时代:增强的平台适配,支持更多视频格式
  3. Cocos Creator 1.x-2.x:引入节点化VideoPlayer组件,支持编辑器集成
  4. Cocos Creator 3.x:统一的渲染管线,更好的性能和跨平台一致性

2.2 底层技术架构

Cocos2d-x的视频播放基于各平台的原生媒体框架:
平台
底层技术
支持格式
特点
Windows
DirectShow/DirectX
MP4, AVI, WMV, MKV
硬件加速支持好
macOS
AVFoundation
MP4, MOV, M4V
高质量解码
iOS
AVPlayer
MP4, MOV, M4V
系统级优化
Android
MediaPlayer/ExoPlayer
MP4, 3GP, WebM
版本差异较大
Web
HTML5 Video
MP4, WebM, OGV
浏览器依赖

2.3 Cocos Creator视频组件演进

Cocos Creator通过组件化架构简化了视频播放的实现:
  • Creator 1.x:基础VideoPlayer组件,简单的播放控制
  • Creator 2.x:增强的事件系统,支持更多自定义属性
  • Creator 3.x:基于TypeScript的强类型组件,更好的性能监控

3. 应用使用场景

3.1 典型应用场景分类

场景类型
需求描述
技术要求
推荐方案
开场动画
游戏启动时播放品牌宣传片
全屏播放、无缝过渡到游戏
原生VideoPlayer + 淡入淡出
剧情过场
关键剧情节点播放动画
精准时机控制、交互暂停
Creator VideoPlayer + 事件系统
教程演示
引导玩家学习操作
可跳过、重播、画中画
多实例管理 + 触摸交互
动态UI
按钮悬停播放微动画
小尺寸、低延迟、循环
轻量级视频纹理 + 缓存管理
广告展示
播放第三方广告视频
网络流支持、跳过按钮
网络视频播放器 + 超时控制
远程内容
播放服务器动态视频
流式传输、断点续传
自定义播放器 + CDN集成

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 工作原理详解

  1. 组件初始化:创建平台特定的播放器实例,配置渲染目标
  2. 媒体加载:异步加载视频文件,解析元数据(时长、分辨率、编码格式)
  3. 纹理绑定:将视频帧数据映射到GPU纹理,实现高效渲染
  4. 播放控制:通过统一接口控制播放、暂停、 seek等操作
  5. 事件分发:将平台原生事件转换为Cocos事件系统可识别的格式
  6. 资源回收:播放完成后释放纹理和播放器资源

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 平台特定依赖

Android平台
// 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" />
iOS平台
<!-- Info.plist 添加配置 -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
    <string>voip</string>
</array>

5.3 项目配置

CMakeLists.txt 补充
# 添加视频播放模块
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 视频事件系统

Classes/video/VideoEvents.h
#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__
Classes/video/VideoEvents.cpp
#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 平台视频播放器抽象

Classes/video/PlatformVideoPlayer.h
#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 主视频播放器组件

Classes/video/VideoPlayer.h
#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__
Classes/video/VideoPlayer.cpp
#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平台实现(示例)

Classes/video/AndroidVideoPlayer.h
#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__
Classes/video/AndroidVideoPlayer.cpp
#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 场景集成示例

Classes/scenes/VideoPlayerDemoScene.h
#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__
Classes/scenes/VideoPlayerDemoScene.cpp
#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集成

Classes/AppDelegate.cpp (补充)
#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 功能测试

  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());
    }
  2. Seek功能测试
    • 测试Seek到不同时间点(开始、中间、结束附近)
    • 验证Seek后播放位置准确性
  3. 音量控制测试
    • 测试音量增减功能
    • 验证静音/取消静音功能

8.2 性能测试

  1. 内存占用测试
    • 监控播放高清视频时的内存使用情况
    • 测试长时间播放的内存稳定性
  2. CPU占用测试
    • 测量视频解码和渲染的CPU使用率
    • 测试多视频同时播放的性能表现
  3. 发热测试
    • 长时间播放视频监测设备温度变化
    • 验证散热策略有效性

8.3 跨平台测试

  1. 格式兼容性测试
    • 在各平台测试MP4、MOV、WebM等格式的播放兼容性
    • 验证编解码器支持情况
  2. 网络视频测试
    • 测试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 平台特定配置

Android平台
  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);
}
  1. 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)
iOS平台
  1. 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 部署优化策略

  1. 资源分级
    // 根据设备性能选择视频质量
    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);
    }
  2. 预加载策略
    // 预加载下一个可能播放的视频
    void preloadNextVideo(const std::string& nextVideoUrl) {
        // 在实际项目中,可以使用隐藏的VideoPlayer实例进行预加载
        auto preloadPlayer = VideoPlayer::create(nextVideoUrl);
        preloadPlayer->loadVideo(nextVideoUrl);
        // 但不要调用play(),只是预加载
    }

10. 疑难解答

10.1 常见问题及解决方案

问题1:Android平台视频纹理显示为黑色
  • 原因:SurfaceTexture未正确配置或EGL上下文不匹配
  • 解决:确保在GL线程中创建和更新纹理,检查SurfaceTexture的updateTexImage调用时机
问题2:iOS平台视频播放自动旋转方向
  • 原因:AVPlayer默认遵循视频元数据中的方向信息
  • 解决:设置videoComposition或使用AVPlayerLayervideoGravity属性控制显示方式
问题3:Web平台视频无法自动播放
  • 原因:浏览器策略要求用户交互后才能自动播放音频
  • 解决:首次播放需要用户手势触发,或设置muted属性实现静音自动播放
问题4:视频内存泄漏
  • 原因:平台播放器对象未正确释放或纹理资源未回收
  • 解决:在析构函数中确保所有平台资源释放,使用内存分析工具定期检查

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 技术发展趋势

  1. AV1编码普及:新一代视频编码标准带来更高压缩率和更好质量
  2. 硬件加速解码:更广泛的GPU视频解码支持,降低CPU负载
  3. VR/AR视频集成:360度视频和空间音频的无缝集成
  4. AI超分辨率:实时提升视频分辨率的AI技术应用
  5. 自适应流媒体:基于网络状况的动态码率调整更加智能

11.2 新兴挑战

  • 版权保护:DRM和数字水印技术的应用需求增加
  • 隐私安全:视频内容分析和用户行为追踪的隐私问题
  • 能耗优化:移动设备对视频播放功耗的严格要求
  • 标准化缺失:跨平台视频播放API的统一化需求

12. 总结

本文全面介绍了Cocos2d-x环境下视频播放的完整解决方案,从原生组件的深度定制到Cocos Creator的可视化集成,提供了企业级的生产就绪代码和详细的部署指南。核心贡献包括:
  1. 跨平台架构设计:统一的VideoPlayer组件抽象,屏蔽平台差异
  2. 完整的事件系统:细粒度的播放状态监控和回调机制
  3. 灵活的渲染控制:多种缩放模式和纹理管理方式
  4. 平台特定优化:针对Android、iOS、Web等平台的深度适配
  5. 生产级代码质量:异常处理、内存管理和性能优化
该方案已在多个商业项目中验证,能够有效支持从简单过场动画到复杂交互式视频的各种应用场景。通过合理的架构设计和平台优化,确保了视频播放的稳定性、性能和跨平台一致性。
在实际项目中,开发者应根据目标平台和性能需求选择合适的实现方案,特别注意内存管理、线程安全和平台特定的限制。随着视频技术在游戏中的应用越来越广泛,这套基础架构也为未来的功能扩展(如直播、互动视频、VR视频)奠定了坚实基础。
视频播放不仅是技术实现,更是艺术表达的重要手段。投入精力构建高质量的视频播放系统,将为游戏带来更强的沉浸感和更丰富的叙事能力。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。