0
0
Lập trình
NM

Phát Video bằng FFmpeg trong WGPU Native

Đăng vào 6 ngày trước

• 9 phút đọc

Giới thiệu

Trong bài viết này, chúng ta sẽ khám phá cách sử dụng API Dawn để nhập các textures bên ngoài vào WebGPU, đặc biệt là cách nhập một luồng video được tạo bởi FFmpeg trong ngữ cảnh DirectX11. Nếu bạn thích theo dõi bài viết này qua video, có một phiên bản YouTube đi kèm.

Tài liệu tham khảo

Dưới đây là một số liên kết liên quan đến bài viết này:

Tổng quan dự án

Để triển khai tính năng này, tôi đã xây dựng một thành phần VideoPlayer trong NervLand engine. Dưới đây là ví dụ về cách sử dụng thành phần này để phát một video ghi lại phiên chơi game từ "Ghost of Tsushima" trong ứng dụng thử nghiệm TerrainView của tôi. Chúng ta sẽ phát video ngay khi ứng dụng khởi động mà không có các tính năng như tạm dừng, tua lại hay thay đổi kích thước cửa sổ, nhưng đây là khởi đầu tốt.

Tôi đã chuẩn bị một thư mục trong kho NervLand Adventures để chia sẻ các tệp liên quan đến tính năng nhập video này. Nếu bạn quan tâm, hãy xem kho lưu trữ này tại đây. Lưu ý rằng các tệp nguồn này sẽ không biên dịch ngay lập tức nhưng vẫn có thể hữu ích như một tài liệu tham khảo để bạn triển khai tương tự.

Giới thiệu về FFmpeg Video Playback

Chúng ta bắt đầu với một tệp video, trong trường hợp này là một bản ghi gameplay ở định dạng mp4 với mã hóa x264. Sau đó, chúng ta sẽ sử dụng thư viện FFmpeg để tải tệp này, cấu hình bộ giải mã để sử dụng phần cứng và tạo ra luồng hình ảnh video trực tiếp trên GPU trong ngữ cảnh DirectX. Cuối cùng, chúng ta sẽ sao chép Texture DirectX kết quả vào ngữ cảnh WebGPU bằng API xử lý Shared Texture mới từ Dawn. Và cuối cùng, chúng ta có thể xử lý Texture WGPU cuối cùng như bất kỳ texture nào khác và hiển thị nó trong ứng dụng.

Hạn chế hiện tại

Đây là một triển khai rất đơn giản và còn nhiều hạn chế. Trong mã NervLand, tôi đã thêm một lớp VideoPlayer, trong đó có một lớp trừu tượng VideoDecoder. Tiếp theo, tôi đã thêm một triển khai cụ thể gọi là FFMPEGVideoDecoder, phụ thuộc vào các nhị phân FFmpeg, và vì vậy, tôi hiện chỉ đang xây dựng mô-đun này trong phiên bản native của engine. Điều này có nghĩa là hiện tại không có hỗ trợ cho tính năng phát video này khi tôi xây dựng bằng Emscripten.

Hơn nữa, triển khai hiện tại chỉ được cung cấp cho Windows và yêu cầu rõ ràng rằng chúng ta sử dụng backend DirectX 12 ở phía Dawn và định dạng DirectX 11 Video Acceleration ở phía FFmpeg. Vẫn còn nhiều điều để kiểm tra và điều tra về chủ đề này.

Phân tích mã

Thiết lập "Video Surface"

Trong ứng dụng TerrainView, tôi đang bắt đầu triển khai dần một hệ thống cấu hình nhằm tạo ra nội dung cảnh bổ sung trên hành tinh. Đây không phải là phần trực tiếp của lớp phát video, nhưng để tham khảo, đây là phần tử YAML mà tôi sử dụng để yêu cầu tạo một quad đơn giản tại một vị trí cụ thể trên mặt đất, nơi chúng ta sẽ phát luồng video:

yaml Copy
video_surface:
  type: VideoSurfaceBlueprint
  node: quads0
  reference_point: phantom_rp01
  position: Vec3d(0.0, 2.0, 0.0)
  ypr: Vec3d(0.0, 0.0, 0.0)
  anchor: bottom | center
  video_file: D:\Temp\Videos\Ghost_of_Tsushima_v1.mp4
  width: 3.0
  aspect: 16/9

Tiếp theo, chúng ta sẽ sử dụng VideoSurfaceBlueprint này để xây dựng một "đối tượng video surface" thực tế trong phương thức:

cpp Copy
void VideoSurfaceBlueprint::construct_blueprint(Scene& scene) {
    // Lấy node mà quad này sẽ được gắn vào:
    auto& node = scene.get_node_by_id<QuadsNode>(_node);

    // Lấy vị trí thế giới với offset vị trí:
    auto wpos = calculate_world_position();

    auto& bsp = node.get_texture();
    BSPArea area = create_video_or_fallback_area(bsp);

    // Thiết lập vị trí Quad:
    setup_quad_location(bsp, area);

    // Thêm quad tại vị trí đó:
    node.add_quad(wpos, _quadLoc, _persistent);
}

Trong phương thức này, bước quan trọng đối với chúng ta là gọi create_video_or_fallback_area(), nếu tệp video được tìm thấy, nó sẽ dẫn đến việc gọi phương thức chuyên dụng này:

cpp Copy
auto VideoSurfaceBlueprint::create_video_area(WGPUBSPTexture& bsp) -> BSPArea {
    logDEBUG("Loading video file: {}", _videoFile);
    auto player = VideoPlayer::create({.videoFile = _videoFile});
    auto width = player->get_width();
    auto height = player->get_height();
    logDEBUG("Creating target BSP area of size {}x{}", width, height);

    // Giữ player như một đối tượng tham chiếu trong engine:
    auto* eng = WGPUEngine::instance();
    eng->set_shared_object(get_id(), player.get());

    auto img = Image::make_random<RGBA8>(width, height);
    auto area = bsp.add_image(img);

    // Gán texture mục tiêu và điểm gốc:
    Vec3u orig(area.rect.xmin, area.rect.ymin, area.layer);
    player->set_target_texture(bsp.get_texture(), orig);

    // Bắt đầu phát video:
    player->play();

    return area;
}

Phân tích chi tiết bộ giải mã FFMpeg

Trong phương thức open_input() của bộ giải mã, chúng ta sẽ gọi phương thức initialize_decoder():

cpp Copy
auto FFMPEGVideoDecoder::initialize_decoder(const char* filename) -> bool {
    // Mở tệp đầu vào
    _formatCtx = avformat_alloc_context();
    if (!_formatCtx) {
        logERROR("Failed to allocate format context");
        return false;
    }

    I32 ret = avformat_open_input(&_formatCtx, filename, nullptr, nullptr);
    if (ret < 0) {
        logDEBUG("Failed to open input file: {}", err2str(ret));
        return false;
    }

    // Nhận thông tin luồng
    ret = avformat_find_stream_info(_formatCtx, nullptr);
    if (ret < 0) {
        logDEBUG("Failed to find stream info: {}", err2str(ret));
        return false;
    }

    // Tìm luồng video
    _videoStreamIdx = av_find_best_stream(_formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
    if (_videoStreamIdx < 0) {
        logDEBUG("No video stream found");
        return false;
    }

    AVStream* video_stream = _formatCtx->streams[_videoStreamIdx];
    const AVCodec* codec = avcodec_find_decoder(video_stream->codecpar->codec_id);
    if (codec == nullptr) {
        logDEBUG("Unsupported codec");
        return false;
    }

    // Tính FPS
    if (video_stream->r_frame_rate.den != 0) {
        _fps = av_q2d(video_stream->r_frame_rate);
    } else if (video_stream->avg_frame_rate.den != 0) {
        _fps = av_q2d(video_stream->avg_frame_rate);
    } else {
        _fps = 25.0; // Giá trị mặc định
    }

    // Lưu kích thước khung hình
    _frameWidth = video_stream->codecpar->width;
    _frameHeight = video_stream->codecpar->height;

    // Cố gắng thiết lập bộ giải mã phần cứng trước
    _isHWAccelerated = false;
    if (_desc.enableHardwareAcceleration && setup_hardware_decoder(codec)) {
        logDEBUG("Hardware acceleration enabled");
        _isHWAccelerated = true;
    } else {
        logDEBUG("Using software decoding");
        // Thay thế bằng bộ giải mã phần mềm
        _codecCtx = avcodec_alloc_context3(codec);
        if (_codecCtx == nullptr) {
            logDEBUG("Failed to allocate codec context");
            return false;
        }
    }

    // Sao chép tham số codec
    ret = avcodec_parameters_to_context(_codecCtx, video_stream->codecpar);
    if (ret < 0) {
        logDEBUG("Failed to copy codec parameters: {}", err2str(ret));
        return false;
    }

    // Mở codec
    ret = avcodec_open2(_codecCtx, codec, nullptr);
    if (ret < 0) {
        logDEBUG("Failed to open codec: {}", err2str(ret));
        return false;
    }

    return true;
}

Vòng lặp phát VideoPlayer

Với bộ giải mã FFMpeg đã sẵn sàng, việc xử lý tiếp tục với lệnh gọi VideoPlayer::play():

cpp Copy
void VideoPlayer::play() {
    NVCHK(_decoder != nullptr, "Invalid decoder.");

    _isPlaying = true;
    _playbackStartTick = SystemTime::tick();
    _lastUpdateTick = -1;
    _playTime = 0.0;
    _currentFrameIndex = 0;
    logDEBUG("Started playing video {}", _filename);

    auto* eng = WGPUEngine::instance();
    _updateCb = eng->add_pre_render_func([this] { update(); });
};

Trong hàm update(), chúng ta sử dụng thời gian thực thi hiện tại và tỷ lệ FPS video để xác định xem có cần yêu cầu khung hình tiếp theo từ bộ giải mã hay không. Nếu việc giải mã thành công, chúng ta ngay lập tức yêu cầu sao chép khung hình đó vào texture WGPU đã chỉ định.

Mẹo hiệu suất

  • Sử dụng phần cứng: Hãy chắc chắn rằng bạn đã kích hoạt tính năng tăng tốc phần cứng để cải thiện hiệu suất phát video.
  • Quản lý bộ nhớ: Theo dõi và quản lý bộ nhớ hiệu quả để tránh rò rỉ bộ nhớ khi xử lý video.
  • Điều chỉnh FPS: Kiểm tra và điều chỉnh tỷ lệ khung hình để đảm bảo video phát mượt mà.

Các vấn đề thường gặp

  • Video không phát: Kiểm tra xem tệp video có tồn tại và định dạng có được hỗ trợ không.
  • Lỗi codec: Đảm bảo rằng codec video tương thích với FFmpeg và được cài đặt chính xác.

Kết luận

Bài viết này đã cung cấp cái nhìn tổng quan về cách phát video bằng FFmpeg trong ngữ cảnh WGPU. Hy vọng bạn đã học hỏi được điều gì đó bổ ích từ bài viết này. Nếu bạn muốn tìm hiểu thêm về chủ đề này, hãy kiểm tra các tài nguyên tôi đã chia sẻ trong kho NervLand Adventures. Nếu bạn có bất kỳ câu hỏi nào, đừng ngần ngại để lại nhận xét bên dưới!

Câu hỏi thường gặp

  1. Tôi có thể sử dụng FFmpeg trên nền tảng nào?
    • FFmpeg có thể được sử dụng trên nhiều nền tảng khác nhau, bao gồm Windows, macOS và Linux.
  2. Tôi có cần cài đặt thêm thư viện nào không?
    • Có, bạn cần cài đặt FFmpeg và các thư viện liên quan để hỗ trợ phát video.
  3. Làm thế nào để tối ưu hóa hiệu suất phát video?
    • Sử dụng phần cứng tăng tốc và tối ưu hóa quy trình xử lý video trong mã.

Hẹn gặp lại lần sau!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào