Files
BPI-CM9-Linux/aml-patches/amlbian/0003-chromium-add-HW-video-decode-browser-build-recipe-1-.patch

1328 lines
55 KiB
Diff

From fef4bbbfb3ae3a6596990448edcf0686805a88c6 Mon Sep 17 00:00:00 2001
From: "haijun.li" <[email protected]>
Date: Wed, 15 Jul 2026 21:55:49 +0800
Subject: [PATCH 3/7] chromium: add HW video decode browser build recipe [1/1]
SWPL-269930
Add Chromium 132 browser with V4L2 hardware video decoding
and DMA-BUF zero-copy for AML A311Y3.
- build-chromium.sh: fetch, patch, build, package deb
- 17-file minimal patch for V4L2/EGL/format support
- gpu_mojo_media_client_amlogic.cc: VideoDecoderPipeline
- systemd service for decoder DW mode setup
- Controlled by BUILD_UNITS chromium option
Change-Id: I555c9394e5e3ec7788a716c3b8eb9ce5fa3d92f2
Signed-off-by: haijun.li <[email protected]>
---
config/boards/include/amlogic_common.inc | 1 +
extensions/amlogic-overlay.sh | 3 +
lib/function-img.sh | 12 +
scripts/chromium/build-chromium.sh | 446 ++++++++++++
scripts/chromium/config/args.gn | 29 +
.../services/gpu_mojo_media_client_amlogic.cc | 77 ++
.../patches/minimal-hwdecode-final.patch | 660 ++++++++++++++++++
7 files changed, 1228 insertions(+)
create mode 100755 scripts/chromium/build-chromium.sh
create mode 100644 scripts/chromium/config/args.gn
create mode 100644 scripts/chromium/overlay/media/mojo/services/gpu_mojo_media_client_amlogic.cc
create mode 100644 scripts/chromium/patches/minimal-hwdecode-final.patch
diff --git a/config/boards/include/amlogic_common.inc b/config/boards/include/amlogic_common.inc
index 554a39b..5407654 100644
--- a/config/boards/include/amlogic_common.inc
+++ b/config/boards/include/amlogic_common.inc
@@ -69,6 +69,7 @@ BUILD_UNITS=(
screencapture
linux-uapi-headers
mediactrlsrc
+ chromium
hdmi-test
)
else
diff --git a/extensions/amlogic-overlay.sh b/extensions/amlogic-overlay.sh
index cc4b30f..9c1555e 100644
--- a/extensions/amlogic-overlay.sh
+++ b/extensions/amlogic-overlay.sh
@@ -252,6 +252,9 @@ function load_local_source() {
display_alert "AMLOGIC" ">> Start build logcat " "info"
source "${EXTENSION_DIR}/../scripts/devtools/android-tools-logcat/build-logcat.sh"
+ display_alert "AMLOGIC" ">> Start build chromium " "info"
+ source "${EXTENSION_DIR}/../scripts/chromium/build-chromium.sh"
+
if ! is_unit_enabled "rootfs"; then
echo "rootfs NOT in BUILD_UNITS, exit immediately"
exit 1
diff --git a/lib/function-img.sh b/lib/function-img.sh
index 56b7a70..89c9806 100644
--- a/lib/function-img.sh
+++ b/lib/function-img.sh
@@ -358,6 +358,16 @@ function install_linux_uapi_deb {
install_aml_deb $linux_uapi_headers_file
}
+function install_chromium_amlbian() {
+ if ! is_unit_enabled "chromium" 2>/dev/null; then
+ return 0
+ fi
+ local chromium_file=$(find "${BUILD_DEBS}" -name "chromium-browser-aml_*.deb" 2>/dev/null | head -1)
+ if [ -n "$chromium_file" ]; then
+ install_aml_deb $chromium_file
+ fi
+}
+
function install_common() {
display_alert "install amlogic debs: GPU/NPU/DEC/ENC/... ... " "IMG" "info"
@@ -408,6 +418,8 @@ function install_common() {
install_linux_uapi_deb
install_vendor_file_deb
+ install_chromium_amlbian
+
install_tvserver_deb
install_logcat_deb
diff --git a/scripts/chromium/build-chromium.sh b/scripts/chromium/build-chromium.sh
new file mode 100755
index 0000000..d2a2488
--- /dev/null
+++ b/scripts/chromium/build-chromium.sh
@@ -0,0 +1,446 @@
+#!/usr/bin/env bash
+#
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (c) 2026 AMLOGIC
+#
+# Chromium browser build script for AMLbian
+# Hardware video decoding (V4L2) + DMA-BUF zero-copy
+#
+# Controlled by BUILD_UNITS: add "chromium" to enable
+
+# NOTE: set -e is NOT used here. This script modifies PATH/env and runs in a
+# subshell (line ~450) to isolate side-effects from the parent build system.
+# Error handling is explicit (|| return 1) throughout.
+
+# ==============================================================================
+# Section 1: Default Configuration
+# ==============================================================================
+
+CHROMIUM_VERSION="132.0.6834.83"
+DEB_VERSION="${CHROMIUM_VERSION}-aml1"
+DEB_NAME="chromium-browser-aml"
+
+# Get script directory (scripts/chromium/ — two levels below AMLBIAN_DIR)
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+AMLBIAN_DIR="$(dirname "$(dirname "${SCRIPT_DIR}")")"
+PATCHES_DIR="${SCRIPT_DIR}/patches"
+OVERLAY_DIR="${SCRIPT_DIR}/overlay"
+CONFIG_DIR="${SCRIPT_DIR}/config"
+
+# Source common functions if available and not already loaded
+if [[ -z "${__FUNCTION_COMMON_LOADED:-}" ]] && [[ -f "${AMLBIAN_DIR}/lib/function-common.sh" ]]; then
+ source "${AMLBIAN_DIR}/lib/function-common.sh"
+fi
+
+: "${CHROMIUM_BUILD_ROOT:=${AMLBIAN_DIR}/../chromium}"
+: "${DEPOT_TOOLS_DIR:=${CHROMIUM_BUILD_ROOT}/depot_tools}"
+# CHROMIUM_DL_DIR: optional env var (Jenkins node config or manual).
+# Tarball search order: CHROMIUM_DL_DIR → CHROMIUM_MIRROR_DIR → local cache → CDN.
+: "${CHROMIUM_MIRROR_DIR:=/mnt/fileroot/Public/mirror}"
+CHROMIUM_SRC="${CHROMIUM_BUILD_ROOT}/chromium-${CHROMIUM_VERSION}"
+OUT_DIR="${CHROMIUM_SRC}/out/arm64"
+# Armbian sets SRC to the build dir. BUILD_DEBS must match ${SRC}/output/
+# which is where install_aml_deb looks for debs in Docker.
+if [[ -n "${SRC}" ]] && [[ -d "${SRC}/output" ]]; then
+ BUILD_DEBS="${SRC}/output"
+else
+ BUILD_DEBS="${AMLBIAN_DIR}/../output"
+fi
+
+: "${NINJA_JOBS:=$(nproc)}"
+
+# Fallback display_alert for standalone testing
+if ! command -v display_alert &>/dev/null; then
+ display_alert() { echo "[$2] $1 ${3:+($3)}"; }
+fi
+
+# ==============================================================================
+# Section 2: Internal Helper
+# ==============================================================================
+
+# Find a file in multiple directories. Sets REPLY to the first hit.
+function __chromium_find_in_dirs() {
+ local filename="$1"; shift
+ REPLY=""
+ local dir
+ for dir in "$@"; do
+ [[ -z "${dir}" ]] && continue
+ if [[ -f "${dir}/${filename}" ]]; then
+ REPLY="${dir}/${filename}"
+ return 0
+ fi
+ done
+ return 1
+}
+
+# ==============================================================================
+# Section 3: Build Functions
+# ==============================================================================
+
+function __chromium_check_enabled() {
+ # When running standalone (no BUILD_UNITS), always build
+ if ! command -v is_unit_enabled &>/dev/null; then
+ return 0
+ fi
+ if ! is_unit_enabled "chromium"; then
+ display_alert "CHROMIUM" "Not in BUILD_UNITS, skipping" "info"
+ return 1
+ fi
+ return 0
+}
+
+function __chromium_fetch_source() {
+ display_alert "CHROMIUM" "Fetching source ${CHROMIUM_VERSION}" "info"
+
+ mkdir -p "${CHROMIUM_BUILD_ROOT}"
+
+ # If source dir exists but is incomplete (any state: empty, only out/, partial),
+ # remove it so we get a clean extraction.
+ if [[ -d "${CHROMIUM_SRC}" ]] && [[ ! -f "${CHROMIUM_SRC}/.gn" ]]; then
+ display_alert "CHROMIUM" "Incomplete source tree at ${CHROMIUM_SRC} — removing" "wrn"
+ rm -rf "${CHROMIUM_SRC}"
+ fi
+
+ # Source already complete
+ if [[ -f "${CHROMIUM_SRC}/.gn" ]]; then
+ display_alert "CHROMIUM" "Source complete at ${CHROMIUM_SRC}" "info"
+ return 0
+ fi
+
+ # Find tarball: CHROMIUM_DL_DIR (Jenkins) → mirror → local cache
+ local tarball="chromium-${CHROMIUM_VERSION}.tar.xz"
+ local tarball_path=""
+
+ if __chromium_find_in_dirs "${tarball}" \
+ "${CHROMIUM_DL_DIR}" \
+ "${CHROMIUM_MIRROR_DIR}" \
+ "${CHROMIUM_BUILD_ROOT}"; then
+ tarball_path="${REPLY}"
+ display_alert "CHROMIUM" "Found tarball: ${tarball_path}" "info"
+ else
+ # Last resort: download from Google CDN
+ tarball_path="${CHROMIUM_BUILD_ROOT}/${tarball}"
+ display_alert "CHROMIUM" "Downloading ${tarball} from Google CDN (6.4 GB) ..." "info"
+ if ! wget -q --show-progress \
+ "https://commondatastorage.googleapis.com/chromium-browser-official/${tarball}" \
+ -O "${tarball_path}"; then
+ display_alert "CHROMIUM" "ERROR: download failed" "err"
+ rm -f "${tarball_path}"
+ return 1
+ fi
+ fi
+
+ display_alert "CHROMIUM" "Extracting ${tarball_path} ..." "info"
+ if ! tar -xf "${tarball_path}" -C "${CHROMIUM_BUILD_ROOT}"; then
+ display_alert "CHROMIUM" "ERROR: tar extraction failed" "err"
+ rm -rf "${CHROMIUM_SRC}"
+ return 1
+ fi
+
+ if [[ ! -f "${CHROMIUM_SRC}/.gn" ]]; then
+ display_alert "CHROMIUM" "ERROR: .gn missing after extraction (CHROMIUM_SRC=${CHROMIUM_SRC})" "err"
+ return 1
+ fi
+ display_alert "CHROMIUM" "Source ready at ${CHROMIUM_SRC}" "info"
+}
+
+function __chromium_apply_patches() {
+ display_alert "CHROMIUM" "Applying patches" "info"
+ cd "${CHROMIUM_SRC}" || { display_alert "CHROMIUM" "ERROR: Cannot cd to ${CHROMIUM_SRC}" "err"; return 1; }
+
+ # Apply minimal HW decode patch (use patch -p1, source tree has no git repo)
+ if [[ -f "${PATCHES_DIR}/minimal-hwdecode-final.patch" ]]; then
+ if patch -p1 --dry-run -s < "${PATCHES_DIR}/minimal-hwdecode-final.patch" 2>/dev/null; then
+ # --batch: never ask interactively (CI safe)
+ patch -p1 --batch < "${PATCHES_DIR}/minimal-hwdecode-final.patch" || \
+ { display_alert "CHROMIUM" "ERROR: patch failed" "err"; return 1; }
+ display_alert "CHROMIUM" "Patch applied" "info"
+ else
+ display_alert "CHROMIUM" "Patch already applied or conflict, skipping" "info"
+ fi
+ fi
+
+ # Copy overlay files
+ if [[ -d "${OVERLAY_DIR}" ]]; then
+ cp -rv "${OVERLAY_DIR}/." "${CHROMIUM_SRC}/"
+ display_alert "CHROMIUM" "Overlay applied" "info"
+ fi
+}
+
+function __chromium_configure() {
+ display_alert "CHROMIUM" "Configuring build" "info"
+
+ # Force system Python — depot_tools bundled Python has wrong prefix (/work/out)
+ export PATH="/usr/bin:${DEPOT_TOOLS_DIR}:${PATH}"
+ unset PYTHONHOME PYTHONPATH
+ export GN_ARGS_USE_SYSTEM_PYTHON=1
+ export VPYTHON_BYPASS="manually managed vpython not supported by nickel"
+ export DEPOT_TOOLS_UPDATE=0
+
+ cd "${CHROMIUM_SRC}" || {
+ display_alert "CHROMIUM" "ERROR: Cannot cd to ${CHROMIUM_SRC}" "err"
+ return 1
+ }
+ # Resolve canonical path — CHROMIUM_SRC may contain ".." when sourced by Armbian
+ # (e.g. amlbian/../chromium/...). gn fails to find .gn with ".." in OUT_DIR.
+ local src_abs
+ src_abs="$(pwd)"
+ local out_abs="${src_abs}/out/arm64"
+
+ if [[ ! -f "${src_abs}/.gn" ]]; then
+ display_alert "CHROMIUM" "ERROR: .gn not found at ${src_abs} — source tree missing" "err"
+ return 1
+ fi
+
+ # Toolchains: clang, rust, sysroot.
+ # The source tarball doesn't include these. Strategy:
+ # 1. Reuse from existing build tree at ${CHROMIUM_BUILD_ROOT}/src/ (same chromium version)
+ # 2. Download from Google CDN via update scripts
+ local existing_src="${CHROMIUM_BUILD_ROOT}/src"
+ local clang_dir="${src_abs}/third_party/llvm-build"
+ local rust_dir="${src_abs}/third_party/rust-toolchain"
+ local sysroot_dir="${src_abs}/build/linux/debian_bullseye_arm64-sysroot"
+
+ # Expected clang revision (used to verify correct version)
+ local expected_clang_rev
+ expected_clang_rev=$(grep "^CLANG_REVISION = " "${src_abs}/tools/clang/scripts/update.py" | cut -d"'" -f2)
+
+ # --- Clang ---
+ if ! grep -q "${expected_clang_rev}" "${clang_dir}/Release+Asserts/cr_build_revision" 2>/dev/null; then
+ if [[ -d "${existing_src}/third_party/llvm-build/Release+Asserts/bin" ]] && \
+ grep -q "${expected_clang_rev}" "${existing_src}/third_party/llvm-build/Release+Asserts/cr_build_revision" 2>/dev/null; then
+ display_alert "CHROMIUM" "Linking clang from existing build" "info"
+ rm -rf "${clang_dir}"
+ ln -snf "${existing_src}/third_party/llvm-build" "${clang_dir}"
+ else
+ display_alert "CHROMIUM" "Downloading clang via update.py" "info"
+ python3 "${src_abs}/tools/clang/scripts/update.py" || {
+ display_alert "CHROMIUM" "ERROR: clang setup failed" "err"; return 1; }
+ fi
+ fi
+
+ # --- Rust ---
+ if [[ ! -x "${rust_dir}/bin/rustc" ]]; then
+ if [[ -x "${existing_src}/third_party/rust-toolchain/bin/rustc" ]]; then
+ display_alert "CHROMIUM" "Linking rust from existing build" "info"
+ rm -rf "${rust_dir}"
+ ln -snf "${existing_src}/third_party/rust-toolchain" "${rust_dir}"
+ else
+ display_alert "CHROMIUM" "Downloading rust via update_rust.py" "info"
+ python3 "${src_abs}/tools/rust/update_rust.py" || {
+ display_alert "CHROMIUM" "ERROR: rust setup failed" "err"; return 1; }
+ fi
+ fi
+
+ # --- Sysroot (arm64 cross-compilation) ---
+ if [[ ! -d "${sysroot_dir}" ]]; then
+ if [[ -d "${existing_src}/build/linux/debian_bullseye_arm64-sysroot" ]]; then
+ display_alert "CHROMIUM" "Linking arm64 sysroot from existing build" "info"
+ ln -snf "${existing_src}/build/linux/debian_bullseye_arm64-sysroot" "${sysroot_dir}"
+ else
+ display_alert "CHROMIUM" "Installing arm64 sysroot via install-sysroot.py" "info"
+ python3 "${src_abs}/build/linux/sysroot_scripts/install-sysroot.py" --arch=arm64 || {
+ display_alert "CHROMIUM" "ERROR: sysroot installation failed" "err"; return 1; }
+ fi
+ fi
+
+ mkdir -p "${out_abs}"
+ cp "${CONFIG_DIR}/args.gn" "${out_abs}/args.gn"
+
+ local gn_bin="${src_abs}/buildtools/linux64/gn"
+ if [[ ! -x "${gn_bin}" ]]; then
+ display_alert "CHROMIUM" "ERROR: gn not found at ${gn_bin}" "err"
+ return 1
+ fi
+ "${gn_bin}" gen "${out_abs}" || {
+ display_alert "CHROMIUM" "ERROR: gn gen failed" "err"
+ return 1
+ }
+ display_alert "CHROMIUM" "Configured: ${out_abs}" "info"
+}
+
+function __chromium_compile() {
+ display_alert "CHROMIUM" "Building (jobs=${NINJA_JOBS})" "info"
+
+ cd "${CHROMIUM_SRC}" || { display_alert "CHROMIUM" "ERROR: Cannot cd to ${CHROMIUM_SRC}" "err"; return 1; }
+ local src_abs
+ src_abs="$(pwd)"
+ "${src_abs}/third_party/ninja/ninja" -C "${src_abs}/out/arm64" -j "${NINJA_JOBS}" chrome || {
+ display_alert "CHROMIUM" "ERROR: ninja build failed" "err"
+ return 1
+ }
+ display_alert "CHROMIUM" "Build complete" "info"
+}
+
+function __chromium_package_deb() {
+ display_alert "CHROMIUM" "Packaging ${DEB_NAME} ${DEB_VERSION}" "info"
+
+ mkdir -p "${BUILD_DEBS}"
+ local pkg_root="${BUILD_DEBS}/.chromium-pkg-$$"
+ rm -rf "${pkg_root}"
+ mkdir -p "${pkg_root}"
+ local install_dir="${pkg_root}/usr/lib/chromium"
+ local bin_dir="${pkg_root}/usr/bin"
+ local app_dir="${pkg_root}/usr/share/applications"
+ local limits_dir="${pkg_root}/etc/security/limits.d"
+ local profile_dir="${pkg_root}/etc/profile.d"
+
+ mkdir -p "${install_dir}/locales" "${bin_dir}" "${app_dir}" \
+ "${limits_dir}" "${profile_dir}"
+
+ local src
+ src="$(realpath "${OUT_DIR}")"
+ display_alert "CHROMIUM" "Packaging from: ${src}" "info"
+
+ # Binary + resources
+ if [[ ! -f "${src}/chrome" ]]; then
+ display_alert "CHROMIUM" "ERROR: ${src}/chrome not found!" "err"
+ return 1
+ fi
+ install -m 755 "${src}/chrome" "${install_dir}/chrome"
+ for f in "${src}"/icudtl.dat "${src}"/*.pak "${src}"/*.bin; do
+ [[ -f "${f}" ]] && install -m 644 "${f}" "${install_dir}/"
+ done
+ # ANGLE libraries (required for GPU process)
+ for f in "${src}"/libGLESv2.so "${src}"/libEGL.so; do
+ [[ -f "${f}" ]] && install -m 644 "${f}" "${install_dir}/"
+ done
+ # Crashpad handler
+ [[ -f "${src}/chrome_crashpad_handler" ]] && \
+ install -m 755 "${src}/chrome_crashpad_handler" "${install_dir}/"
+ [[ -f "${src}/locales/en-US.pak" ]] && \
+ install -m 644 "${src}/locales/en-US.pak" "${install_dir}/locales/"
+
+ # Launcher script
+ cat > "${bin_dir}/chromium-browser" << 'LAUNCHER'
+#!/bin/sh
+export PULSE_SERVER=unix:/run/user/1000/pulse/native
+ulimit -l unlimited 2>/dev/null
+exec /usr/lib/chromium/chrome \
+ --ozone-platform=wayland --no-sandbox --use-gl=angle --use-angle=gles \
+ --ignore-gpu-blocklist --disable-gpu-process-crash-limit \
+ --disable-features=WebGPU \
+ --enable-features=AcceleratedVideoDecodeLinux,AcceleratedVideoDecodeLinuxGL,ChromeOSHWAV1Decoder \
+ --double-buffer-compositing --disable-partial-raster --lang=en-US \
+ "$@" 2>/tmp/chrome_stderr.log
+LAUNCHER
+ chmod 755 "${bin_dir}/chromium-browser"
+
+ # Desktop entry
+ cat > "${app_dir}/chromium-browser.desktop" << 'DESKTOP'
+[Desktop Entry]
+Version=1.0
+Name=Chromium Browser
+Exec=chromium-browser %U
+Terminal=false
+Type=Application
+Icon=chromium-browser
+Categories=Network;WebBrowser;
+MimeType=text/html;text/xml;application/xhtml+xml;
+DESKTOP
+
+ # memlock limit
+ echo "* - memlock unlimited" > "${limits_dir}/chromium-memlock.conf"
+
+ # HW decode sysfs setup (runs at login)
+ cat > "${profile_dir}/aml-hwdecode.sh" << 'HWDEC'
+#!/bin/sh
+for m in amvdec_vp9 amvdec_vp9_v4l amvdec_h265 amvdec_h265_v4l amvdec_av1 amvdec_av1_v4l; do
+ echo "double_write_mode=16" > /sys/module/$m/parameters/params 2>/dev/null
+done
+echo performance > /sys/devices/platform/fd000000.avalon/devfreq/fd000000.avalon/governor 2>/dev/null
+HWDEC
+ chmod 644 "${profile_dir}/aml-hwdecode.sh"
+
+ # Systemd service for DW mode + GPU freq (needs root)
+ local svc_dir="${pkg_root}/etc/systemd/system"
+ mkdir -p "${svc_dir}"
+ cat > "${svc_dir}/chromium-hwdecode.service" << 'SVC'
+[Unit]
+Description=Chromium HW decode setup (DW mode + GPU freq)
+After=multi-user.target
+
+[Service]
+Type=oneshot
+ExecStart=/bin/sh -c 'for m in amvdec_mh264 amvdec_mh264_v4l amvdec_vp9 amvdec_vp9_v4l amvdec_h265 amvdec_h265_v4l amvdec_av1 amvdec_av1_v4l; do echo "double_write_mode=16" > /sys/module/$m/parameters/params 2>/dev/null; done; echo performance > /sys/devices/platform/fd000000.avalon/devfreq/fd000000.avalon/governor 2>/dev/null'
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
+SVC
+
+ # Enable service via postinst
+ mkdir -p "${pkg_root}/DEBIAN"
+ cat > "${pkg_root}/DEBIAN/postinst" << 'POSTINST'
+#!/bin/sh
+systemctl daemon-reload
+systemctl enable chromium-hwdecode.service
+systemctl start chromium-hwdecode.service 2>/dev/null || true
+POSTINST
+ chmod 755 "${pkg_root}/DEBIAN/postinst"
+
+ # DEBIAN control
+ cat > "${pkg_root}/DEBIAN/control" << EOF
+Package: ${DEB_NAME}
+Version: ${DEB_VERSION}
+Architecture: arm64
+Maintainer: AMLbian <[email protected]>
+Depends: libc6, libglib2.0-0, libnss3, libatk1.0-0, libatk-bridge2.0-0, libcups2, libdrm2, libgbm1, libpango-1.0-0, libcairo2, libasound2, libxkbcommon0, libwayland-client0, pipewire-pulse | pulseaudio, aml-libgles-eabihf-avalon-wayland-drm
+Description: Chromium ${CHROMIUM_VERSION} with AML HW video decode
+ V4L2 hardware decoding with DMA-BUF zero-copy for Amlogic SoCs.
+ Supports H.264, VP9, HEVC up to 4K@60fps.
+EOF
+
+ # Build deb
+ mkdir -p "${BUILD_DEBS}"
+ local deb_file="${BUILD_DEBS}/${DEB_NAME}_${DEB_VERSION}_arm64.deb"
+ dpkg-deb --build --root-owner-group "${pkg_root}" "${deb_file}"
+ rm -rf "${pkg_root}"
+
+ display_alert "CHROMIUM" "Package: ${deb_file}" "info"
+ ls -lh "${deb_file}"
+}
+
+# ==============================================================================
+# Section 4: Main Build Function
+# ==============================================================================
+
+function amlbian_build_chromium() {
+ __chromium_check_enabled || return 0
+
+ # Check if pre-built deb already exists (skip compilation)
+ local existing_deb
+ existing_deb=$(find "${BUILD_DEBS}" -name "${DEB_NAME}_*.deb" 2>/dev/null | head -1)
+ if [[ -n "$existing_deb" ]] && [[ -s "$existing_deb" ]]; then
+ local size
+ size=$(stat -c%s "$existing_deb" 2>/dev/null || echo 0)
+ if [[ $size -gt 1000000 ]]; then
+ display_alert "CHROMIUM" "Pre-built deb found: $(basename "$existing_deb") ($(($size/1024/1024))MB), skipping build" "info"
+ return 0
+ fi
+ fi
+
+ display_alert "CHROMIUM" "No pre-built deb found, compiling from source..." "info"
+ __chromium_fetch_source || { display_alert "CHROMIUM" "ABORT: fetch failed" "err"; return 1; }
+ __chromium_apply_patches || { display_alert "CHROMIUM" "ABORT: patch failed" "err"; return 1; }
+ __chromium_configure || { display_alert "CHROMIUM" "ABORT: configure failed" "err"; return 1; }
+ __chromium_compile || { display_alert "CHROMIUM" "ABORT: compile failed" "err"; return 1; }
+ __chromium_package_deb || { display_alert "CHROMIUM" "ABORT: package failed" "err"; return 1; }
+}
+
+# ==============================================================================
+# Section 5: Main
+# ==============================================================================
+
+display_alert "AMLbian Chromium Build Script" "Version: ${CHROMIUM_VERSION}" "info"
+
+# Run in subshell to isolate PATH/env changes from parent build system.
+# Other build scripts (kernel, uboot) run directly with set -e, but chromium
+# modifies PATH (depot_tools), PYTHONHOME, etc. which would affect subsequent
+# scripts if not isolated.
+(amlbian_build_chromium)
+
+# ==============================================================================
+# End of build-chromium.sh
+# ==============================================================================
diff --git a/scripts/chromium/config/args.gn b/scripts/chromium/config/args.gn
new file mode 100644
index 0000000..bd5b6ea
--- /dev/null
+++ b/scripts/chromium/config/args.gn
@@ -0,0 +1,29 @@
+target_cpu="arm64"
+target_os="linux"
+use_ozone=true
+ozone_auto_platforms=false
+ozone_platform_headless=true
+ozone_platform_wayland=true
+ozone_platform_x11=false
+use_xkbcommon=true
+use_gtk=false
+use_system_libwayland=false
+use_system_freetype=false
+use_system_libffi=false
+is_amlogic_media=true
+mojo_media_services=["renderer","cdm","audio_decoder","audio_encoder","video_decoder"]
+mojo_media_host="gpu"
+ffmpeg_branding="Chrome"
+proprietary_codecs=true
+enable_platform_hevc=true
+enable_hevc_parser_and_hw_decoder=true
+is_official_build=true
+is_debug=false
+dcheck_always_on=false
+is_clang=true
+use_sysroot=true
+chrome_pgo_phase=0
+treat_warnings_as_errors=false
+use_v4l2_codec=true
+use_thin_lto=false
+widevine_version=0
diff --git a/scripts/chromium/overlay/media/mojo/services/gpu_mojo_media_client_amlogic.cc b/scripts/chromium/overlay/media/mojo/services/gpu_mojo_media_client_amlogic.cc
new file mode 100644
index 0000000..75fe4b4
--- /dev/null
+++ b/scripts/chromium/overlay/media/mojo/services/gpu_mojo_media_client_amlogic.cc
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
+//
+// Copyright (c) 2026 Amlogic, Inc. All rights reserved.
+//
+// AML hardware video decoder - zero-copy via VideoDecoderPipeline
+// V4L2 DMA-BUF -> MailboxVideoFrameConverter -> GPU texture -> zero-copy
+// NM12 (NV12 multi-planar) preferred over NM21 for Mali compatibility
+
+#include "media/mojo/services/gpu_mojo_media_client.h"
+
+#include "base/logging.h"
+#include "media/base/media_log.h"
+#include "media/base/video_codecs.h"
+#include "media/base/video_decoder.h"
+#include "media/gpu/chromeos/mailbox_video_frame_converter.h"
+#include "media/gpu/chromeos/platform_video_frame_pool.h"
+#include "media/gpu/chromeos/video_decoder_pipeline.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace media {
+
+class GpuMojoMediaClientAmlogic final : public GpuMojoMediaClient {
+ public:
+ GpuMojoMediaClientAmlogic(GpuMojoMediaClientTraits& traits)
+ : GpuMojoMediaClient(traits) {}
+ ~GpuMojoMediaClientAmlogic() final = default;
+
+ protected:
+ std::unique_ptr<VideoDecoder> CreatePlatformVideoDecoder(
+ VideoDecoderTraits& traits) final {
+ auto frame_pool = std::make_unique<PlatformVideoFramePool>();
+ auto frame_converter = MailboxVideoFrameConverter::Create(
+ gpu_task_runner_, traits.get_command_buffer_stub_cb);
+
+ // NM12 (NV12 multi-planar) first - Mali supports NV12 import
+ // NM21 (NV21 multi-planar) last - Mali crashes on NV21 import
+ return VideoDecoderPipeline::Create(
+ gpu_workarounds_, traits.task_runner, std::move(frame_pool),
+ std::move(frame_converter),
+ {Fourcc(Fourcc::NM12), Fourcc(Fourcc::NV12),
+ Fourcc(Fourcc::NM21), Fourcc(Fourcc::NV21)},
+ traits.media_log->Clone(), /*oop_video_decoder=*/{},
+ /*in_video_decoder_process=*/false);
+ }
+
+ void NotifyPlatformDecoderSupport(
+ mojo::PendingRemote<stable::mojom::StableVideoDecoder> oop_video_decoder,
+ base::OnceCallback<void(
+ mojo::PendingRemote<stable::mojom::StableVideoDecoder>)> cb) final {
+ std::move(cb).Run(std::move(oop_video_decoder));
+ }
+
+ std::optional<SupportedVideoDecoderConfigs>
+ GetPlatformSupportedVideoDecoderConfigs() final {
+ SupportedVideoDecoderConfigs configs;
+ configs.emplace_back(H264PROFILE_BASELINE, H264PROFILE_HIGH,
+ gfx::Size(64, 64), gfx::Size(3840, 2160), false, false);
+ configs.emplace_back(VP9PROFILE_PROFILE0, VP9PROFILE_PROFILE2,
+ gfx::Size(64, 64), gfx::Size(3840, 2160), false, false);
+ configs.emplace_back(HEVCPROFILE_MAIN, HEVCPROFILE_MAIN10,
+ gfx::Size(64, 64), gfx::Size(3840, 2160), false, false);
+ configs.emplace_back(AV1PROFILE_PROFILE_MAIN, AV1PROFILE_PROFILE_MAIN,
+ gfx::Size(64, 64), gfx::Size(3840, 2160), false, false);
+ return configs;
+ }
+
+ VideoDecoderType GetPlatformDecoderImplementationType() final {
+ return VideoDecoderType::kV4L2;
+ }
+};
+
+std::unique_ptr<GpuMojoMediaClient> CreateGpuMediaService(
+ GpuMojoMediaClientTraits& traits) {
+ return std::make_unique<GpuMojoMediaClientAmlogic>(traits);
+}
+
+} // namespace media
diff --git a/scripts/chromium/patches/minimal-hwdecode-final.patch b/scripts/chromium/patches/minimal-hwdecode-final.patch
new file mode 100644
index 0000000..d4d9a5d
--- /dev/null
+++ b/scripts/chromium/patches/minimal-hwdecode-final.patch
@@ -0,0 +1,660 @@
+diff --git a/content/renderer/media/gpu/gpu_video_accelerator_factories_impl.cc b/content/renderer/media/gpu/gpu_video_accelerator_factories_impl.cc
+index 3acfcf5749..45b18d4203 100644
+--- a/content/renderer/media/gpu/gpu_video_accelerator_factories_impl.cc
++++ b/content/renderer/media/gpu/gpu_video_accelerator_factories_impl.cc
+@@ -183,6 +183,9 @@ void GpuVideoAcceleratorFactoriesImpl::DestroyContext() {
+ }
+
+ bool GpuVideoAcceleratorFactoriesImpl::IsGpuVideoDecodeAcceleratorEnabled() {
++#if BUILDFLAG(IS_AMLOGIC_MEDIA)
++ return true;
++#endif
+ return video_decode_accelerator_enabled_;
+ }
+ bool GpuVideoAcceleratorFactoriesImpl::IsGpuVideoEncodeAcceleratorEnabled() {
+diff --git a/content/renderer/render_thread_impl.cc b/content/renderer/render_thread_impl.cc
+index c848b7f8f7..e2b5215d17 100644
+--- a/content/renderer/render_thread_impl.cc
++++ b/content/renderer/render_thread_impl.cc
+@@ -1,3 +1,4 @@
++#include "media/mojo/buildflags.h"
+ // Copyright 2012 The Chromium Authors
+ // Use of this source code is governed by a BSD-style license that can be
+ // found in the LICENSE file.
+@@ -1026,8 +1027,10 @@ media::GpuVideoAcceleratorFactories* RenderThreadImpl::GetGpuFactories() {
+ // Currently, VideoResourceUpdater can't convert hardware resources to
+ // software resources in software compositing mode. So, fall back to software
+ // video decoding if gpu compositing is off.
++#if !BUILDFLAG(IS_AMLOGIC_MEDIA)
+ if (is_gpu_compositing_disabled_)
+ return nullptr;
++#endif
+ // This context is only used to create textures and mailbox them, so
+ // use lower limits than the default.
+ gpu::SharedMemoryLimits limits = gpu::SharedMemoryLimits::ForMailboxContext();
+@@ -1045,6 +1048,9 @@ media::GpuVideoAcceleratorFactories* RenderThreadImpl::GetGpuFactories() {
+ viz::command_buffer_metrics::ContextType::MEDIA,
+ kGpuStreamIdMedia, kGpuStreamPriorityMedia);
+
++#if BUILDFLAG(IS_AMLOGIC_MEDIA)
++ const bool enable_video_decode_accelerator = true;
++#else
+ const bool enable_video_decode_accelerator =
+ #if BUILDFLAG(IS_LINUX)
+ base::FeatureList::IsEnabled(media::kAcceleratedVideoDecodeLinux) &&
+@@ -1053,6 +1059,7 @@ media::GpuVideoAcceleratorFactories* RenderThreadImpl::GetGpuFactories() {
+ (gpu_channel_host->gpu_feature_info()
+ .status_values[gpu::GPU_FEATURE_TYPE_ACCELERATED_VIDEO_DECODE] ==
+ gpu::kGpuFeatureStatusEnabled);
++#endif
+
+ const bool enable_video_encode_accelerator =
+ #if BUILDFLAG(IS_LINUX)
+diff --git a/media/base/format_utils.cc b/media/base/format_utils.cc
+index 0e1080e0a6..98a221b2f4 100644
+--- a/media/base/format_utils.cc
++++ b/media/base/format_utils.cc
+@@ -65,6 +65,9 @@ std::optional<gfx::BufferFormat> VideoPixelFormatToGfxBufferFormat(
+ case PIXEL_FORMAT_NV12:
+ return gfx::BufferFormat::YUV_420_BIPLANAR;
+
++ case PIXEL_FORMAT_NV21:
++ return gfx::BufferFormat::YUV_420_BIPLANAR;
++
+ case PIXEL_FORMAT_NV12A:
+ return gfx::BufferFormat::YUVA_420_TRIPLANAR;
+
+diff --git a/media/base/supported_types.cc b/media/base/supported_types.cc
+index 34e1ecb868..bc0cd98fee 100644
+--- a/media/base/supported_types.cc
++++ b/media/base/supported_types.cc
+@@ -315,7 +315,7 @@ bool IsDecoderAACSupported(const AudioType& type) {
+ return true;
+ }
+ #if BUILDFLAG(ENABLE_MOJO_AUDIO_DECODER) && \
+- (BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN))
++ (BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_AMLOGIC_MEDIA))
+ return GetSupplementalDecoderAudioTypeCache()->IsProfileSupported(type);
+ #else
+ return false;
+@@ -350,12 +350,13 @@ bool IsDecoderDolbyAc3Eac3Supported(const AudioType& type) {
+
+ bool IsDecoderDolbyAc4Supported(const AudioType& type) {
+ #if BUILDFLAG(ENABLE_PLATFORM_AC4_AUDIO) && \
+- BUILDFLAG(ENABLE_MOJO_AUDIO_DECODER) && BUILDFLAG(IS_WIN)
++ BUILDFLAG(ENABLE_MOJO_AUDIO_DECODER) && (BUILDFLAG(IS_WIN) || BUILDFLAG(IS_AMLOGIC_MEDIA))
+ return GetSupplementalDecoderAudioTypeCache()->IsProfileSupported(type);
+ #else
+ return false;
+ #endif // BUILDFLAG(ENABLE_PLATFORM_AC4_AUDIO) &&
+- // BUILDFLAG(ENABLE_MOJO_AUDIO_DECODER) && BUILDFLAG(IS_WIN)
++ // BUILDFLAG(ENABLE_MOJO_AUDIO_DECODER) && (BUILDFLAG(IS_WIN) ||
++ // BUILDFLAG(IS_AMLOGIC_MEDIA))
+ }
+
+ bool IsEncoderH264ProfileSupported(const VideoType& type) {
+@@ -579,6 +580,11 @@ bool IsDefaultEncoderSupportedVideoType(const VideoType& type) {
+ }
+
+ bool IsDecoderBuiltInVideoCodec(VideoCodec codec) {
++#if BUILDFLAG(IS_AMLOGIC_MEDIA)
++ // On Amlogic platforms, all codecs are treated as built-in since the
++ // hardware decoders are used.
++ return true;
++#endif // BUILDFLAG(IS_AMLOGIC_MEDIA)
+ #if BUILDFLAG(ENABLE_FFMPEG_VIDEO_DECODERS) && BUILDFLAG(USE_PROPRIETARY_CODECS)
+ if (codec == VideoCodec::kH264 &&
+ base::FeatureList::IsEnabled(kBuiltInH264Decoder)) {
+diff --git a/media/gpu/chromeos/mailbox_video_frame_converter.cc b/media/gpu/chromeos/mailbox_video_frame_converter.cc
+index 6cbc6d89f5..21994b5f34 100644
+--- a/media/gpu/chromeos/mailbox_video_frame_converter.cc
++++ b/media/gpu/chromeos/mailbox_video_frame_converter.cc
+@@ -389,10 +389,22 @@ void MailboxVideoFrameConverter::WrapSharedImageAndVideoFrameAndOutput(
+ frame->metadata().needs_detiling
+ ? frame->coded_size()
+ : GetRectSizeFromOrigin(frame->visible_rect());
++ // AML: NV21 and NV12 have identical layout for GPU texture import.
++ // WrapSharedImage doesn't support NV21, so use NV12 instead.
++ VideoPixelFormat wrap_format = frame->format();
++ if (wrap_format == PIXEL_FORMAT_NV21)
++ wrap_format = PIXEL_FORMAT_NV12;
+ scoped_refptr<VideoFrame> mailbox_frame = VideoFrame::WrapSharedImage(
+- frame->format(), shared_image, shared_image_sync_token,
++ wrap_format, shared_image, shared_image_sync_token,
+ std::move(release_mailbox_cb), coded_size, frame->visible_rect(),
+ frame->natural_size(), frame->timestamp());
++ if (!mailbox_frame) {
++ DVLOG(3) << "[AML-CONV] WrapSharedImage returned null! format="
++ << VideoPixelFormatToString(frame->format())
++ << " coded=" << coded_size.ToString()
++ << " visible=" << frame->visible_rect().ToString();
++ return;
++ }
+ mailbox_frame->set_color_space(frame->ColorSpace());
+ mailbox_frame->set_hdr_metadata(frame->hdr_metadata());
+ mailbox_frame->set_metadata(frame->metadata());
+@@ -496,8 +508,13 @@ bool MailboxVideoFrameConverter::GenerateSharedImageOnGPUThread(
+ }
+
+ auto gpu_memory_buffer_handle = origin_frame->CreateGpuMemoryBufferHandle();
+- DCHECK(!gpu_memory_buffer_handle.is_null());
+- DCHECK_EQ(gpu_memory_buffer_handle.type, gfx::NATIVE_PIXMAP);
++ DVLOG(3) << "[AML-CONV] CreateGpuMemoryBufferHandle: null="
++ << gpu_memory_buffer_handle.is_null()
++ << " type=" << gpu_memory_buffer_handle.type
++ << " planes=" << gpu_memory_buffer_handle.native_pixmap_handle.planes.size()
++ << " fmt=" << VideoPixelFormatToString(origin_frame->format())
++ << " size=" << origin_frame->coded_size().ToString();
++ if (gpu_memory_buffer_handle.is_null()) return false;
+
+ // The SharedImage size ultimately must correspond to the size used to import
+ // the decoded frame into a graphics API (e.g., the EGL image size when using
+@@ -531,18 +548,18 @@ bool MailboxVideoFrameConverter::GenerateSharedImageOnGPUThread(
+ shared_image_usage |= gpu::SHARED_IMAGE_USAGE_WEBGPU_READ;
+ }
+
++ DVLOG(3) << "[AML-CONV] About to CreateSharedImage size=" << shared_image_size.ToString();
+ scoped_refptr<gpu::ClientSharedImage> client_shared_image =
+ gpu_delegate_->CreateSharedImage(std::move(gpu_memory_buffer_handle),
+ GetSharedImageFormat(*buffer_format),
+ shared_image_size, src_color_space,
+ kTopLeft_GrSurfaceOrigin,
+ kPremul_SkAlphaType, shared_image_usage);
++ DVLOG(3) << "[AML-CONV] CreateSharedImage result: " << (client_shared_image ? "OK" : "FAIL");
+ if (!client_shared_image) {
+ OnError(FROM_HERE, "Failed to create shared image.");
+ return false;
+ }
+- // There's no need to UpdateSharedImage() after CreateSharedImage().
+-
+ shared_image->Reset(std::move(client_shared_image));
+ return true;
+ }
+diff --git a/media/gpu/chromeos/native_pixmap_frame_resource.cc b/media/gpu/chromeos/native_pixmap_frame_resource.cc
+index 09c763a7e8..5ac815656b 100644
+--- a/media/gpu/chromeos/native_pixmap_frame_resource.cc
++++ b/media/gpu/chromeos/native_pixmap_frame_resource.cc
+@@ -3,6 +3,7 @@
+ // found in the LICENSE file.
+
+ #include "media/gpu/chromeos/native_pixmap_frame_resource.h"
++#include <drm_fourcc.h>
+
+ #include "atomic"
+
+@@ -130,10 +131,8 @@ scoped_refptr<NativePixmapFrameResource> NativePixmapFrameResource::Create(
+ std::move(dmabuf_fds[i]));
+ }
+
+- // This is only ever called with V4L2-allocated buffers, so |layout.modifier|
+- // is expected to be kNoModifier.
+- CHECK_EQ(layout.modifier(), gfx::NativePixmapHandle::kNoModifier);
+- handle.modifier = layout.modifier();
++ // Keep kNoModifier for V4L2 MMAP buffers.
++ handle.modifier = gfx::NativePixmapHandle::kNoModifier; // AML: force no modifier for V4L2 MMAP
+
+ // Note: |buffer_usage| is not set. As a result, the constructed
+ // NativePixmapFrameResource cannot be converted to a VideoFrame with the
+diff --git a/media/gpu/chromeos/video_decoder_pipeline.cc b/media/gpu/chromeos/video_decoder_pipeline.cc
+--- a/media/gpu/chromeos/video_decoder_pipeline.cc
++++ b/media/gpu/chromeos/video_decoder_pipeline.cc
+@@ -3,6 +3,7 @@
+ // found in the LICENSE file.
+
+ #include "media/gpu/chromeos/video_decoder_pipeline.h"
++#include "media/mojo/buildflags.h"
+
+ #include <memory>
+ #include <optional>
+@@ -571,6 +572,7 @@
+
+ // Make sure that the configuration requested is supported by the driver,
+ // which must provide such information.
++#if !BUILDFLAG(IS_AMLOGIC_MEDIA)
+ const auto supported_configs =
+ supported_configs_for_testing_.empty()
+ ? VideoDecoderPipeline::GetSupportedConfigs(GetDecoderType(),
+@@ -588,6 +590,7 @@
+ std::move(init_cb).Run(DecoderStatus::Codes::kUnsupportedConfig);
+ return;
+ }
++#endif // !BUILDFLAG(IS_AMLOGIC_MEDIA)
+ constexpr auto kVideoCodecProfileCount = media::VIDEO_CODEC_PROFILE_MAX + 1;
+ base::UmaHistogramEnumeration(
+ "Media.PlatformVideoDecoding.VideoCodecProfile", config.profile(),
+@@ -647,8 +650,10 @@
+ get_original_frame_cb = base::BindRepeating(
+ &OOPVideoDecoder::GetOriginalFrame,
+ base::Unretained(static_cast<OOPVideoDecoder*>(decoder_.get())));
++ } else if (!main_frame_pool_) {
++ // AML: PickDecoderOutputFormat() drops the pool in V4L2 MMAP mode.
++ get_original_frame_cb = base::NullCallback();
+ } else {
+- CHECK(main_frame_pool_);
+ PlatformVideoFramePool* platform_video_frame_pool =
+ main_frame_pool_->AsPlatformVideoFramePool();
+ // The only |frame_converter_| that needs the GetOriginalFrameCB callback
+@@ -1058,6 +1063,15 @@
+ // vector (CAPCM*1_Sand_E.h264).
+ CHECK_LE(num_codec_reference_frames, 32u);
+
++ DVLOG(3) << "[AML-PIPE] PickDecoderOutputFormat: candidates=" << candidates.size()
++ << " renderable=" << renderable_fourccs_.size();
++ for (const auto& c : candidates) {
++ DVLOG(3) << "[AML-PIPE] candidate: " << c.fourcc.ToString()
++ << " size=" << c.size.ToString();
++ }
++ for (const auto& f : renderable_fourccs_) {
++ DVLOG(3) << "[AML-PIPE] renderable: " << f.ToString();
++ }
+ if (candidates.empty())
+ return CroStatus::Codes::kNoDecoderOutputFormatCandidates;
+
+@@ -1069,6 +1083,8 @@
+ // provided by the frame pool matches the one supported by the |decoder_|, we
+ // don't need an image processor.
+ std::optional<PixelLayoutCandidate> viable_candidate;
++ DVLOG(3) << "[AML-PIPE] output_size=" << (output_size ? output_size->ToString() : "none")
++ << " decoder_visible=" << decoder_visible_rect.ToString();
+ if (!output_size || *output_size == decoder_visible_rect.size()) {
+ for (const auto& fourcc : renderable_fourccs_) {
+ for (const auto& candidate : candidates) {
+@@ -1108,14 +1124,9 @@
+ *allocator, VideoFrame::STORAGE_DMABUFS);
+ #elif BUILDFLAG(IS_LINUX) && BUILDFLAG(USE_V4L2_CODEC)
+ // Linux w/ V4L2 should not use a custom allocator
+- // Only tested with video_decode_accelerator_tests
+- // TODO(wenst@) Test with full Chromium Browser
+ CHECK(!allocator.has_value());
+ if (viable_candidate) {
+- // Instead, let V4L2 allocate the buffers if it can decode directly
+- // to the preferred formats. There's no need to allocate frames.
+- // This is not compatible with VdVideoDecodeAccelerator, which
+- // expects GPU buffers in VdVideoDecodeAccelerator::GetPicture()
++ // Let V4L2 allocate the buffers (MMAP mode)
+ frame_converter_->set_get_original_frame_cb(base::NullCallback());
+ main_frame_pool_.reset();
+ return *viable_candidate;
+diff --git a/media/gpu/v4l2/legacy/v4l2_video_decoder_backend_stateful.cc b/media/gpu/v4l2/legacy/v4l2_video_decoder_backend_stateful.cc
+--- a/media/gpu/v4l2/legacy/v4l2_video_decoder_backend_stateful.cc
++++ b/media/gpu/v4l2/legacy/v4l2_video_decoder_backend_stateful.cc
+@@ -464,7 +464,17 @@
+ // Wrap the frame into another one so we can be signaled when the
+ // consumer is done with it and reuse the V4L2 buffer.
+ scoped_refptr<FrameResource> origin_frame = buffer->GetFrameResource();
++ DVLOG(3) << "[AML-V4L2] OnOutputBufferDequeued: origin_frame="
++ << (origin_frame ? "valid" : "NULL")
++ << " memory=" << output_queue_->GetMemoryType();
++ if (!origin_frame) {
++ // Queue was reset (resolution change). Skip this buffer.
++ return;
++ }
+ frame = origin_frame->CreateWrappingFrame();
++ if (!frame) {
++ return;
++ }
+ frame->AddDestructionObserver(base::BindOnce(
+ &V4L2StatefulVideoDecoderBackend::ReuseOutputBufferThunk,
+ task_runner_, weak_this_, buffer));
+@@ -761,6 +771,8 @@
+ #endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
+ V4L2_PIX_FMT_VP8,
+ V4L2_PIX_FMT_VP9,
++ // AML: without AV01 here every AV1 stream is rejected in Initialize().
++ V4L2_PIX_FMT_AV1,
+ };
+ auto device = base::MakeRefCounted<V4L2Device>();
+ VideoDecodeAccelerator::SupportedProfiles profiles =
+diff --git a/media/gpu/v4l2/v4l2_queue.cc b/media/gpu/v4l2/v4l2_queue.cc
+index bd6b3f5278..4242e06e38 100644
+--- a/media/gpu/v4l2/v4l2_queue.cc
++++ b/media/gpu/v4l2/v4l2_queue.cc
+@@ -133,13 +133,12 @@ std::vector<base::ScopedFD> GetDmabufsForV4L2Buffer(
+ expbuf.type = buf_type;
+ expbuf.index = index;
+ expbuf.plane = i;
+- expbuf.flags = O_CLOEXEC;
++ expbuf.flags = O_CLOEXEC | O_RDWR; // AML: Mali needs RW access
+ if (ioctl_cb.Run(VIDIOC_EXPBUF, &expbuf) != 0) {
+ RecordVidiocIoctlErrorUMA(VidiocIoctlRequests::kVidiocExpbuf);
+ dmabuf_fds.clear();
+ break;
+ }
+-
+ dmabuf_fds.push_back(base::ScopedFD(expbuf.fd));
+ }
+
+@@ -268,6 +267,15 @@ bool V4L2Buffer::Query() {
+ return false;
+ }
+
++ // Log actual plane info from QUERYBUF
++ for (size_t pi = 0; pi < v4l2_buffer_.length; pi++) {
++ LOG(ERROR) << "[AML-QB] buf=" << v4l2_buffer_.index
++ << " plane" << pi
++ << " length=" << v4l2_planes_[pi].length
++ << " offset=" << v4l2_planes_[pi].m.mem_offset
++ << " bytesused=" << v4l2_planes_[pi].bytesused;
++ }
++
+ DCHECK(plane_mappings_.size() == v4l2_buffer_.length);
+
+ return true;
+@@ -314,7 +322,10 @@ size_t V4L2Buffer::GetMemoryUsage() const {
+ scoped_refptr<FrameResource> V4L2Buffer::CreateFrame() {
+ auto layout = V4L2FormatToVideoFrameLayout(format_);
+ if (!layout) {
+- VLOGF(1) << "Cannot create frame layout for V4L2 buffers";
++ DVLOG(3) << "[AML-V4L2] CreateFrame: layout failed, pixfmt=0x"
++ << std::hex << format_.fmt.pix_mp.pixelformat << std::dec
++ << " " << format_.fmt.pix_mp.width << "x" << format_.fmt.pix_mp.height
++ << " num_planes=" << format_.fmt.pix_mp.num_planes;
+ return nullptr;
+ }
+
+@@ -322,7 +333,8 @@ scoped_refptr<FrameResource> V4L2Buffer::CreateFrame() {
+ ioctl_cb_, v4l2_buffer_.index, v4l2_buffer_.length,
+ static_cast<enum v4l2_buf_type>(v4l2_buffer_.type));
+ if (dmabuf_fds.empty()) {
+- VLOGF(1) << "Failed to get DMABUFs of V4L2 buffer";
++ DVLOG(3) << "[AML-V4L2] CreateFrame: GetDmabufs failed, index="
++ << v4l2_buffer_.index << " length=" << v4l2_buffer_.length;
+ return nullptr;
+ }
+
+@@ -355,15 +367,19 @@ scoped_refptr<FrameResource> V4L2Buffer::CreateFrame() {
+ const scoped_refptr<FrameResource>& V4L2Buffer::GetFrameResource() {
+ // We can create the FrameResource only when using MMAP buffers.
+ if (v4l2_buffer_.memory != V4L2_MEMORY_MMAP) {
+- VLOGF(1) << "Cannot create video frame from non-MMAP buffer";
+- // Allow NOTREACHED() on invalid argument because this is an internal
+- // method.
+- NOTREACHED();
++ DVLOG(3) << "[AML-V4L2] GetFrameResource: memory type is "
++ << v4l2_buffer_.memory << " (not MMAP=" << V4L2_MEMORY_MMAP << ")"
++ << " index=" << v4l2_buffer_.index;
++ static const scoped_refptr<FrameResource> null_res;
++ return null_res;
+ }
+
+ // Create the video frame instance if requiring it for the first time.
+ if (!frame_) {
+ frame_ = CreateFrame();
++ if (!frame_) {
++ DVLOG(3) << "[AML-V4L2] CreateFrame returned null for buffer " << v4l2_buffer_.index;
++ }
+ }
+
+ return frame_;
+@@ -1178,6 +1194,12 @@ size_t V4L2Queue::AllocateBuffers(size_t count,
+ return 0;
+ }
+ planes_count_ = format->fmt.pix_mp.num_planes;
++ // AML 6.12: queue_setup returns 2 vb2 planes for NV21, but G_FMT
++ // reports num_planes=1. Force 2 so QUERYBUF gets correct length.
++ if (type_ == V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE && planes_count_ < 2) {
++ planes_count_ = 2;
++ format->fmt.pix_mp.num_planes = 2;
++ }
+ DCHECK_LE(planes_count_, static_cast<size_t>(VIDEO_MAX_PLANES));
+
+ __u8 flags = incoherent ? V4L2_MEMORY_FLAG_NON_COHERENT : 0;
+diff --git a/media/gpu/v4l2/v4l2_utils.cc b/media/gpu/v4l2/v4l2_utils.cc
+--- a/media/gpu/v4l2/v4l2_utils.cc
++++ b/media/gpu/v4l2/v4l2_utils.cc
+@@ -17,6 +17,10 @@
+ #if BUILDFLAG(IS_CHROMEOS)
+ #include <linux/media/av1-ctrls.h>
+ #endif
++// AML: the Debian uAPI headers predate the AV1 controls.
++#ifndef V4L2_CID_MPEG_VIDEO_AV1_PROFILE
++#define V4L2_CID_MPEG_VIDEO_AV1_PROFILE (V4L2_CID_CODEC_BASE + 656)
++#endif
+
+ #include "base/containers/contains.h"
+ #include "base/metrics/histogram_functions.h"
+@@ -264,8 +268,17 @@
+ planes.reserve(num_color_planes);
+ for (size_t i = 0; i < num_buffers; ++i) {
+ const v4l2_plane_pix_format& plane_format = pix_mp.plane_fmt[i];
+- planes.emplace_back(static_cast<int32_t>(plane_format.bytesperline), 0u,
+- plane_format.sizeimage);
++ int32_t stride = static_cast<int32_t>(plane_format.bytesperline);
++ size_t sizeimage = plane_format.sizeimage;
++ // AML: When num_planes was forced to 2 for NV21, plane_fmt[1] may
++ // have zero values. Fill stride/size from plane0.
++ // Each vb2 plane is a separate DMA-BUF allocation, so offset=0 for all.
++ if (i > 0 && stride == 0 && num_buffers == 2) {
++ stride = static_cast<int32_t>(pix_mp.plane_fmt[0].bytesperline);
++ sizeimage = stride * pix_mp.height / 2;
++ }
++ // AML: each EXPBUF plane is independent DMA-BUF, offset=0 for each
++ planes.emplace_back(stride, 0u, sizeimage);
+ }
+ // For the case that #color planes > #buffers, it fills stride of color
+ // plane which does not map to buffer.
+@@ -338,10 +351,9 @@
+ {V4L2_PIX_FMT_VP8_FRAME, V4L2_CID_MPEG_VIDEO_VP8_PROFILE},
+ {V4L2_PIX_FMT_VP9, V4L2_CID_MPEG_VIDEO_VP9_PROFILE},
+ {V4L2_PIX_FMT_VP9_FRAME, V4L2_CID_MPEG_VIDEO_VP9_PROFILE},
+-#if BUILDFLAG(IS_CHROMEOS)
++ // AML: needed on Linux too, or no AV1 profile is ever enumerated.
+ {V4L2_PIX_FMT_AV1, V4L2_CID_MPEG_VIDEO_AV1_PROFILE},
+ {V4L2_PIX_FMT_AV1_FRAME, V4L2_CID_MPEG_VIDEO_AV1_PROFILE},
+-#endif
+ };
+
+ // Default VideoCodecProfiles associated to a V4L2 Codec Control ID.
+@@ -359,9 +371,8 @@
+ #endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
+ {V4L2_CID_MPEG_VIDEO_VP8_PROFILE, {VP8PROFILE_ANY}},
+ {V4L2_CID_MPEG_VIDEO_VP9_PROFILE, {VP9PROFILE_PROFILE0}},
+-#if BUILDFLAG(IS_CHROMEOS)
++ // AML: fallback used when the driver has no AV1 profile control.
+ {V4L2_CID_MPEG_VIDEO_AV1_PROFILE, {AV1PROFILE_PROFILE_MAIN}},
+-#endif
+ };
+
+ // Correspondence from a VideoCodecProfiles to V4L2 codec described
+@@ -379,10 +390,9 @@
+ {VP8PROFILE_ANY, MAKE_V4L2_CODEC_PAIR(V4L2_PIX_FMT_VP8, FRAME)},
+ {VP9PROFILE_PROFILE0, MAKE_V4L2_CODEC_PAIR(V4L2_PIX_FMT_VP9, FRAME)},
+ {VP9PROFILE_PROFILE2, MAKE_V4L2_CODEC_PAIR(V4L2_PIX_FMT_VP9, FRAME)},
+-#if BUILDFLAG(IS_CHROMEOS)
++ // AML: the fourcc -> profile direction is not IS_CHROMEOS gated.
+ {AV1PROFILE_PROFILE_MAIN,
+ MAKE_V4L2_CODEC_PAIR(V4L2_PIX_FMT_AV1, FRAME)},
+-#endif
+ };
+
+ } // namespace
+@@ -545,8 +555,11 @@
+
+ uint32_t VideoCodecProfileToV4L2PixFmt(VideoCodecProfile profile,
+ bool slice_based) {
+- CHECK(base::Contains(kVideoCodecProfileToV4L2CodecPixFmt, profile))
+- << "Unsupported profile: " << GetProfileName(profile);
++ // AML: the header contract is INVALID for unmapped profiles, not a crash.
++ if (!base::Contains(kVideoCodecProfileToV4L2CodecPixFmt, profile)) {
++ VLOGF(1) << "Unsupported profile: " << GetProfileName(profile);
++ return V4L2_PIX_FMT_INVALID;
++ }
+
+ const auto& v4l2_pix_fmt = kVideoCodecProfileToV4L2CodecPixFmt.at(profile);
+ return slice_based ? v4l2_pix_fmt.first : v4l2_pix_fmt.second;
+diff --git a/media/gpu/v4l2/v4l2_vda_helpers.cc b/media/gpu/v4l2/v4l2_vda_helpers.cc
+--- a/media/gpu/v4l2/v4l2_vda_helpers.cc
++++ b/media/gpu/v4l2/v4l2_vda_helpers.cc
+@@ -160,7 +160,8 @@
+ #endif // BUILDFLAG(ENABLE_HEVC_PARSER_AND_HW_DECODER)
+ case VideoCodec::kVP8:
+ case VideoCodec::kVP9:
+- // VP8/VP9 don't need any frame splitting, use the default implementation.
++ case VideoCodec::kAV1:
++ // AML: AV1 needs no frame splitting either, one buffer is one temporal unit.
+ return std::make_unique<v4l2_vda_helpers::InputBufferFragmentSplitter>();
+ default:
+ LOG(ERROR) << "Unhandled profile: " << profile;
+diff --git a/media/media_options.gni b/media/media_options.gni
+index e235287ebb..36e13c3e91 100644
+--- a/media/media_options.gni
++++ b/media/media_options.gni
+@@ -347,6 +347,14 @@ declare_args() {
+ chrome_wide_echo_cancellation_supported = is_win || is_mac || is_linux
+ }
+
++declare_args() {
++ is_amlogic_media=false
++}
++
++declare_args() {
++ widevine_version=0
++}
++
+ # Do not expand this list without double-checking with OWNERS, this is a list of
+ # all targets which roll up into the //media component. It controls visibility
+ # of subcomponent targets and public_deps for //media.
+diff --git a/media/mojo/BUILD.gn b/media/mojo/BUILD.gn
+index 9573d77ba8..92ecf99112 100644
+--- a/media/mojo/BUILD.gn
++++ b/media/mojo/BUILD.gn
+@@ -55,6 +55,7 @@ buildflag_header("buildflags") {
+ "ENABLE_MOJO_VIDEO_DECODER=$enable_mojo_video_decoder",
+ "ENABLE_MOJO_MEDIA_IN_BROWSER_PROCESS=$enable_mojo_media_in_browser_process",
+ "ENABLE_MOJO_MEDIA_IN_GPU_PROCESS=$enable_mojo_media_in_gpu_process",
++ "IS_AMLOGIC_MEDIA=$is_amlogic_media",
+ ]
+ }
+
+diff --git a/media/mojo/services/BUILD.gn b/media/mojo/services/BUILD.gn
+index 3e2dac7991..b3911dcf35 100644
+--- a/media/mojo/services/BUILD.gn
++++ b/media/mojo/services/BUILD.gn
+@@ -119,6 +119,11 @@ component("services") {
+ sources += [ "gpu_mojo_media_client_mac.cc" ]
+ } else if (is_win) {
+ sources += [ "gpu_mojo_media_client_win.cc" ]
++ } else if (is_amlogic_media) {
++ sources += [ "gpu_mojo_media_client_amlogic.cc" ]
++ deps += [
++ "//media/gpu/chromeos",
++ ]
+ } else if (use_vaapi || use_v4l2_codec) {
+ if (is_linux) {
+ sources += [ "gpu_mojo_media_client_linux.cc" ]
+diff --git a/media/mojo/services/gpu_mojo_media_client.cc b/media/mojo/services/gpu_mojo_media_client.cc
+index 7a739e1d73..0475c34757 100644
+--- a/media/mojo/services/gpu_mojo_media_client.cc
++++ b/media/mojo/services/gpu_mojo_media_client.cc
+@@ -3,6 +3,7 @@
+ // found in the LICENSE file.
+
+ #include "media/mojo/services/gpu_mojo_media_client.h"
++#include "media/mojo/buildflags.h"
+
+ #include <optional>
+ #include <utility>
+@@ -156,12 +157,16 @@ GpuMojoMediaClient::GetSupportedVideoDecoderConfigs() {
+ if (!supported_config_cache_) {
+ // Only bother to query if accelerated video decoding is enabled.
+ // (RenderMediaClient does not know about GPU features before it asks.)
++#if BUILDFLAG(IS_AMLOGIC_MEDIA)
++ {
++#else
+ if (gpu_preferences_.disable_accelerated_video_decode ||
+ (gpu_feature_info_
+ .status_values[gpu::GPU_FEATURE_TYPE_ACCELERATED_VIDEO_DECODE] !=
+ gpu::kGpuFeatureStatusEnabled)) {
+ supported_config_cache_ = SupportedVideoDecoderConfigs();
+ } else {
++#endif
+ supported_config_cache_ = GetPlatformSupportedVideoDecoderConfigs();
+ }
+
+@@ -230,12 +235,14 @@ std::unique_ptr<VideoDecoder> GpuMojoMediaClient::CreateVideoDecoder(
+ const gfx::ColorSpace& target_color_space,
+ mojo::PendingRemote<stable::mojom::StableVideoDecoder> oop_video_decoder) {
+ // Always respect GPU features.
++#if !BUILDFLAG(IS_AMLOGIC_MEDIA)
+ if (gpu_preferences_.disable_accelerated_video_decode ||
+ (gpu_feature_info_
+ .status_values[gpu::GPU_FEATURE_TYPE_ACCELERATED_VIDEO_DECODE] !=
+ gpu::kGpuFeatureStatusEnabled)) {
+ return nullptr;
+ }
++#endif
+ // All implementations require a command buffer.
+ if (!command_buffer_id)
+ return nullptr;
+diff --git a/ui/ozone/common/BUILD.gn b/ui/ozone/common/BUILD.gn
+index 7ce40f68de..1036e37a91 100644
+--- a/ui/ozone/common/BUILD.gn
++++ b/ui/ozone/common/BUILD.gn
+@@ -42,6 +42,7 @@ source_set("common") {
+ "//ui/gfx/ipc/color",
+ "//ui/gl",
+ "//ui/gl:buildflags",
++ "//media/mojo:buildflags",
+ ]
+
+ data_deps = [ "//third_party/angle:includes" ]
+diff --git a/ui/ozone/common/native_pixmap_egl_binding.cc b/ui/ozone/common/native_pixmap_egl_binding.cc
+index b945e14226..b1b0dda89b 100644
+--- a/ui/ozone/common/native_pixmap_egl_binding.cc
++++ b/ui/ozone/common/native_pixmap_egl_binding.cc
+@@ -8,6 +8,7 @@
+ #endif
+
+ #include "ui/ozone/common/native_pixmap_egl_binding.h"
++#include "media/mojo/buildflags.h"
+
+ #include "base/logging.h"
+ #include "base/memory/scoped_refptr.h"
+@@ -70,7 +71,11 @@ EGLint FourCC(gfx::BufferFormat format) {
+ case gfx::BufferFormat::YVU_420:
+ return DRM_FORMAT_YVU420;
+ case gfx::BufferFormat::YUV_420_BIPLANAR:
++#if BUILDFLAG(IS_AMLOGIC_MEDIA)
++ return 0x3132564e; // DRM_FORMAT_NV21 - AML V4L2 outputs VU order
++#else
+ return DRM_FORMAT_NV12;
++#endif
+ case gfx::BufferFormat::P010:
+ return DRM_FORMAT_P010;
+ case gfx::BufferFormat::RGBA_F16:
+@@ -229,11 +234,19 @@ bool NativePixmapEGLBinding::InitializeFromNativePixmap(
+ attrs.push_back(kPlaneOffsetAttrs[attrs_plane]);
+ attrs.push_back(pixmap->GetDmaBufOffset(attrs_plane));
+ attrs.push_back(kPlanePitchAttrs[attrs_plane]);
+- attrs.push_back(pixmap->GetDmaBufPitch(attrs_plane));
++ {
++ int p = pixmap->GetDmaBufPitch(attrs_plane);
++ if (attrs_plane > 0 && p == 0 && pixmap->GetNumberOfPlanes() == 2)
++ p = pixmap->GetDmaBufPitch(0);
++ attrs.push_back(p);
++ }
+
+ uint64_t modifier = pixmap->GetBufferFormatModifier();
++ // AML: skip modifier only for kNoModifier (V4L2 MMAP video buffers)
++ // GBM-allocated UI buffers have real modifiers and must pass them.
+ if (has_dma_buf_import_modifier &&
+- modifier != gfx::NativePixmapHandle::kNoModifier) {
++ modifier != gfx::NativePixmapHandle::kNoModifier &&
++ modifier != 0) {
+ DCHECK(attrs_plane < std::size(kPlaneLoModifierAttrs));
+ DCHECK(attrs_plane < std::size(kPlaneHiModifierAttrs));
+ attrs.push_back(kPlaneLoModifierAttrs[attrs_plane]);
+@@ -252,7 +265,12 @@ bool NativePixmapEGLBinding::InitializeFromNativePixmap(
+ attrs.push_back(EGL_DMA_BUF_PLANE0_OFFSET_EXT);
+ attrs.push_back(pixmap->GetDmaBufOffset(pixmap_plane));
+ attrs.push_back(EGL_DMA_BUF_PLANE0_PITCH_EXT);
+- attrs.push_back(pixmap->GetDmaBufPitch(pixmap_plane));
++ {
++ int p = pixmap->GetDmaBufPitch(pixmap_plane);
++ if (pixmap_plane > 0 && p == 0)
++ p = pixmap->GetDmaBufPitch(0);
++ attrs.push_back(p);
++ }
+ attrs.push_back(EGL_NONE);
+ }
+
--
2.43.2