feat: DV Profile 7→8.1 RPU conversion via libdovi
Convert Dolby Vision Profile 7 RPU NALUs to Profile 8.1 using libdovi native bridge, preserving dynamic tone mapping on devices with DV8 decoders. Falls back to HEVC stripping when no DV8 decoder is present. - Add JNI bridge (dovi_bridge.cpp) wrapping libdovi's RPU converter - Download prebuilt libdovi from edde746/libdovi-builds releases - DoviConvertingTrackOutput: DV81 mode converts RPUs, HEVC_STRIP strips - Retry logic: if native DV7 decoding fails, auto-retry with conversion - Performance overlay shows active DV conversion mode
This commit is contained in:
@@ -8,6 +8,33 @@ plugins {
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val doviVersion = "2.3.1"
|
||||
val doviDir = layout.buildDirectory.dir("libdovi").get().asFile
|
||||
val doviAbis = mapOf(
|
||||
"arm64-v8a" to "aarch64-linux-android",
|
||||
"armeabi-v7a" to "armv7-linux-androideabi",
|
||||
"x86" to "i686-linux-android",
|
||||
"x86_64" to "x86_64-linux-android",
|
||||
)
|
||||
|
||||
val downloadLibdovi by tasks.registering {
|
||||
val stamp = File(doviDir, ".version")
|
||||
outputs.upToDateWhen { stamp.exists() && stamp.readText().trim() == doviVersion }
|
||||
doLast {
|
||||
doviDir.mkdirs()
|
||||
val baseUrl = "https://github.com/edde746/libdovi-builds/releases/download/v$doviVersion"
|
||||
doviAbis.forEach { (abi, triple) ->
|
||||
val archive = File(doviDir, "$triple.tar.gz")
|
||||
exec { commandLine("curl", "-sfL", "$baseUrl/libdovi-$triple.tar.gz", "-o", archive.absolutePath) }
|
||||
val outDir = File(doviDir, "$abi/lib")
|
||||
outDir.mkdirs()
|
||||
exec { commandLine("tar", "-xzf", archive.absolutePath, "-C", outDir.absolutePath) }
|
||||
archive.delete()
|
||||
}
|
||||
stamp.writeText(doviVersion)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.edde746.plezy"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -31,6 +58,15 @@ android {
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments += listOf(
|
||||
"-DDOVI_ENABLE_LIBDOVI=ON",
|
||||
"-DDOVI_LIBDOVI_PREBUILT_ROOT=${doviDir.absolutePath}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (System.getenv("AMAZON") != null) {
|
||||
versionCode = (flutter.versionCode ?: 0) + 3000
|
||||
ndk {
|
||||
@@ -39,6 +75,12 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
@@ -80,6 +122,11 @@ flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
// Download libdovi before any CMake/native build task
|
||||
tasks.matching { it.name.contains("CMake") || it.name.contains("externalNative") }.configureEach {
|
||||
dependsOn(downloadLibdovi)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("dev.jdtech.mpv:libmpv:0.5.1")
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
project(dovi_bridge)
|
||||
|
||||
option(DOVI_ENABLE_LIBDOVI "Link real libdovi" ON)
|
||||
set(DOVI_LIBDOVI_PREBUILT_ROOT "" CACHE PATH "Path to prebuilt libdovi")
|
||||
|
||||
add_library(dovi_bridge SHARED dovi_bridge.cpp)
|
||||
|
||||
if(DOVI_ENABLE_LIBDOVI)
|
||||
add_library(dovi_static STATIC IMPORTED)
|
||||
set_target_properties(dovi_static PROPERTIES
|
||||
IMPORTED_LOCATION "${DOVI_LIBDOVI_PREBUILT_ROOT}/${ANDROID_ABI}/lib/libdovi.a")
|
||||
target_compile_definitions(dovi_bridge PRIVATE DOVI_REAL_LINKED=1)
|
||||
target_include_directories(dovi_bridge PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
target_link_libraries(dovi_bridge dovi_static log)
|
||||
else()
|
||||
target_compile_definitions(dovi_bridge PRIVATE DOVI_REAL_LINKED=0)
|
||||
target_link_libraries(dovi_bridge log)
|
||||
endif()
|
||||
@@ -0,0 +1,105 @@
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <cstring>
|
||||
|
||||
#define TAG "DoviBridge"
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
|
||||
|
||||
#if DOVI_REAL_LINKED
|
||||
#include "include/libdovi/rpu_parser.h"
|
||||
#endif
|
||||
|
||||
static const char* BRIDGE_VERSION = "1.0.0";
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeConvertDv7RpuToDv81(
|
||||
JNIEnv *env, jclass, jbyteArray payload, jint mode) {
|
||||
#if !DOVI_REAL_LINKED
|
||||
return nullptr;
|
||||
#else
|
||||
if (payload == nullptr) return nullptr;
|
||||
|
||||
jsize len = env->GetArrayLength(payload);
|
||||
if (len <= 0) return nullptr;
|
||||
|
||||
jbyte *buf = env->GetByteArrayElements(payload, nullptr);
|
||||
if (buf == nullptr) return nullptr;
|
||||
|
||||
// Try dovi_parse_unspec62_nalu first (handles escaped NALs), fallback to dovi_parse_rpu
|
||||
DoviRpuOpaque *rpu = dovi_parse_unspec62_nalu(
|
||||
reinterpret_cast<const uint8_t *>(buf), static_cast<size_t>(len));
|
||||
|
||||
if (rpu == nullptr) {
|
||||
env->ReleaseByteArrayElements(payload, buf, JNI_ABORT);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *err = dovi_rpu_get_error(rpu);
|
||||
if (err != nullptr) {
|
||||
// Fallback: try dovi_parse_rpu (raw RPU without NAL framing)
|
||||
dovi_rpu_free(rpu);
|
||||
rpu = dovi_parse_rpu(
|
||||
reinterpret_cast<const uint8_t *>(buf), static_cast<size_t>(len));
|
||||
if (rpu == nullptr) {
|
||||
env->ReleaseByteArrayElements(payload, buf, JNI_ABORT);
|
||||
return nullptr;
|
||||
}
|
||||
err = dovi_rpu_get_error(rpu);
|
||||
if (err != nullptr) {
|
||||
LOGW("RPU parse failed: %s", err);
|
||||
dovi_rpu_free(rpu);
|
||||
env->ReleaseByteArrayElements(payload, buf, JNI_ABORT);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
env->ReleaseByteArrayElements(payload, buf, JNI_ABORT);
|
||||
|
||||
// Convert to target profile (mode 2 = P8.1 with no-op curves)
|
||||
int32_t ret = dovi_convert_rpu_with_mode(rpu, static_cast<uint8_t>(mode));
|
||||
if (ret != 0) {
|
||||
err = dovi_rpu_get_error(rpu);
|
||||
LOGW("RPU conversion failed (mode %d): %s", mode, err ? err : "unknown");
|
||||
dovi_rpu_free(rpu);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Write back as UNSPEC62 NAL
|
||||
const DoviData *out = dovi_write_unspec62_nalu(rpu);
|
||||
if (out == nullptr || out->data == nullptr || out->len == 0) {
|
||||
err = dovi_rpu_get_error(rpu);
|
||||
LOGW("RPU write failed: %s", err ? err : "unknown");
|
||||
if (out != nullptr) dovi_data_free(out);
|
||||
dovi_rpu_free(rpu);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jbyteArray result = env->NewByteArray(static_cast<jsize>(out->len));
|
||||
if (result != nullptr) {
|
||||
env->SetByteArrayRegion(result, 0, static_cast<jsize>(out->len),
|
||||
reinterpret_cast<const jbyte *>(out->data));
|
||||
}
|
||||
|
||||
dovi_data_free(out);
|
||||
dovi_rpu_free(rpu);
|
||||
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeIsConversionPathReady(
|
||||
JNIEnv *, jclass) {
|
||||
#if DOVI_REAL_LINKED
|
||||
return JNI_TRUE;
|
||||
#else
|
||||
return JNI_FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeGetBridgeVersion(
|
||||
JNIEnv *env, jclass) {
|
||||
return env->NewStringUTF(BRIDGE_VERSION);
|
||||
}
|
||||
@@ -0,0 +1,747 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef DOVI_H
|
||||
#define DOVI_H
|
||||
|
||||
|
||||
#define RPU_PARSER_MAJOR 3
|
||||
#define RPU_PARSER_MINOR 3
|
||||
#define RPU_PARSER_PATCH 2
|
||||
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define DoviNUM_COMPONENTS 3
|
||||
|
||||
/**
|
||||
* Opaque Dolby Vision RPU.
|
||||
*
|
||||
* Use dovi_rpu_free to free.
|
||||
* It should be freed regardless of whether or not an error occurred.
|
||||
*/
|
||||
typedef struct DoviRpuOpaque DoviRpuOpaque;
|
||||
|
||||
/**
|
||||
* Struct representing a data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the data buffer
|
||||
*/
|
||||
const uint8_t *data;
|
||||
/**
|
||||
* Data buffer size
|
||||
*/
|
||||
size_t len;
|
||||
} DoviData;
|
||||
|
||||
/**
|
||||
* C struct for rpu_data_header()
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Profile guessed from the values in the header
|
||||
*/
|
||||
uint8_t guessed_profile;
|
||||
/**
|
||||
* Enhancement layer type (FEL or MEL) if the RPU is profile 7
|
||||
* null pointer if not profile 7
|
||||
*/
|
||||
const char *el_type;
|
||||
/**
|
||||
* Deprecated since 3.2.0
|
||||
* The field is not actually part of the RPU header
|
||||
*/
|
||||
uint8_t rpu_nal_prefix;
|
||||
uint8_t rpu_type;
|
||||
uint16_t rpu_format;
|
||||
uint8_t vdr_rpu_profile;
|
||||
uint8_t vdr_rpu_level;
|
||||
bool vdr_seq_info_present_flag;
|
||||
bool chroma_resampling_explicit_filter_flag;
|
||||
uint8_t coefficient_data_type;
|
||||
uint64_t coefficient_log2_denom;
|
||||
uint8_t vdr_rpu_normalized_idc;
|
||||
bool bl_video_full_range_flag;
|
||||
uint64_t bl_bit_depth_minus8;
|
||||
uint64_t el_bit_depth_minus8;
|
||||
uint64_t vdr_bit_depth_minus8;
|
||||
bool spatial_resampling_filter_flag;
|
||||
uint8_t reserved_zero_3bits;
|
||||
bool el_spatial_resampling_filter_flag;
|
||||
bool disable_residual_flag;
|
||||
bool vdr_dm_metadata_present_flag;
|
||||
bool use_prev_vdr_rpu_flag;
|
||||
uint64_t prev_vdr_rpu_id;
|
||||
} DoviRpuDataHeader;
|
||||
|
||||
/**
|
||||
* Struct representing a data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the data buffer. Can be null if length is zero.
|
||||
*/
|
||||
const uint16_t *data;
|
||||
/**
|
||||
* Data buffer size
|
||||
*/
|
||||
size_t len;
|
||||
} DoviU16Data;
|
||||
|
||||
/**
|
||||
* Struct representing a data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the data buffer. Can be null if length is zero.
|
||||
*/
|
||||
const uint64_t *data;
|
||||
/**
|
||||
* Data buffer size
|
||||
*/
|
||||
size_t len;
|
||||
} DoviU64Data;
|
||||
|
||||
/**
|
||||
* Struct representing a data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the data buffer
|
||||
*/
|
||||
const int64_t *data;
|
||||
/**
|
||||
* Data buffer size
|
||||
*/
|
||||
size_t len;
|
||||
} DoviI64Data;
|
||||
|
||||
/**
|
||||
* Struct representing a 2D data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of Data structs
|
||||
*/
|
||||
const DoviI64Data *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviI64Data2D;
|
||||
|
||||
/**
|
||||
* Struct representing a 2D data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of Data structs
|
||||
*/
|
||||
const DoviU64Data *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviU64Data2D;
|
||||
|
||||
typedef struct {
|
||||
DoviU64Data poly_order_minus1;
|
||||
DoviData linear_interp_flag;
|
||||
DoviI64Data2D poly_coef_int;
|
||||
DoviU64Data2D poly_coef;
|
||||
} DoviPolynomialCurve;
|
||||
|
||||
/**
|
||||
* Struct representing a 3D data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of I64Data2D structs
|
||||
*/
|
||||
const DoviI64Data2D *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviI64Data3D;
|
||||
|
||||
/**
|
||||
* Struct representing a 3D data buffer
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of U64Data2D structs
|
||||
*/
|
||||
const DoviU64Data2D *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviU64Data3D;
|
||||
|
||||
typedef struct {
|
||||
DoviData mmr_order_minus1;
|
||||
DoviI64Data mmr_constant_int;
|
||||
DoviU64Data mmr_constant;
|
||||
DoviI64Data3D mmr_coef_int;
|
||||
DoviU64Data3D mmr_coef;
|
||||
} DoviMMRCurve;
|
||||
|
||||
typedef struct {
|
||||
/**
|
||||
* [2, 9]
|
||||
*/
|
||||
uint64_t num_pivots_minus2;
|
||||
DoviU16Data pivots;
|
||||
/**
|
||||
* Consistent for a component
|
||||
* Luma (component 0): Polynomial = 0
|
||||
* Chroma (components 1 and 2): MMR = 1
|
||||
*/
|
||||
uint8_t mapping_idc;
|
||||
/**
|
||||
* mapping_idc = 0, null pointer otherwise
|
||||
*/
|
||||
const DoviPolynomialCurve *polynomial;
|
||||
/**
|
||||
* mapping_idc = 1, null pointer otherwise
|
||||
*/
|
||||
const DoviMMRCurve *mmr;
|
||||
} DoviReshapingCurve;
|
||||
|
||||
/**
|
||||
* C struct for rpu_data_nlq()
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t nlq_offset[DoviNUM_COMPONENTS];
|
||||
uint64_t vdr_in_max_int[DoviNUM_COMPONENTS];
|
||||
uint64_t vdr_in_max[DoviNUM_COMPONENTS];
|
||||
uint64_t linear_deadzone_slope_int[DoviNUM_COMPONENTS];
|
||||
uint64_t linear_deadzone_slope[DoviNUM_COMPONENTS];
|
||||
uint64_t linear_deadzone_threshold_int[DoviNUM_COMPONENTS];
|
||||
uint64_t linear_deadzone_threshold[DoviNUM_COMPONENTS];
|
||||
} DoviRpuDataNlq;
|
||||
|
||||
/**
|
||||
* C struct for rpu_data_mapping()
|
||||
*/
|
||||
typedef struct {
|
||||
uint64_t vdr_rpu_id;
|
||||
uint64_t mapping_color_space;
|
||||
uint64_t mapping_chroma_format_idc;
|
||||
uint64_t num_x_partitions_minus1;
|
||||
uint64_t num_y_partitions_minus1;
|
||||
DoviReshapingCurve curves[DoviNUM_COMPONENTS];
|
||||
/**
|
||||
* Set to -1 to represent Option::None
|
||||
*/
|
||||
int32_t nlq_method_idc;
|
||||
/**
|
||||
* Set to -1 to represent Option::None
|
||||
*/
|
||||
int32_t nlq_num_pivots_minus2;
|
||||
/**
|
||||
* Length of zero when not present. Only present in profile 4 and 7.
|
||||
*/
|
||||
DoviU16Data nlq_pred_pivot_value;
|
||||
/**
|
||||
* Pointer to `RpuDataNlq` struct, null if not dual layer profile
|
||||
*/
|
||||
const DoviRpuDataNlq *nlq;
|
||||
} DoviRpuDataMapping;
|
||||
|
||||
/**
|
||||
* Statistical analysis of the frame: min, max, avg brightness.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t min_pq;
|
||||
uint16_t max_pq;
|
||||
uint16_t avg_pq;
|
||||
} DoviExtMetadataBlockLevel1;
|
||||
|
||||
/**
|
||||
* Creative intent trim passes per target display peak brightness
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t target_max_pq;
|
||||
uint16_t trim_slope;
|
||||
uint16_t trim_offset;
|
||||
uint16_t trim_power;
|
||||
uint16_t trim_chroma_weight;
|
||||
uint16_t trim_saturation_gain;
|
||||
int16_t ms_weight;
|
||||
} DoviExtMetadataBlockLevel2;
|
||||
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of ExtMetadataBlockLevel2 structs
|
||||
*/
|
||||
const DoviExtMetadataBlockLevel2 *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviLevel2BlockList;
|
||||
|
||||
/**
|
||||
* Level 1 offsets.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t min_pq_offset;
|
||||
uint16_t max_pq_offset;
|
||||
uint16_t avg_pq_offset;
|
||||
} DoviExtMetadataBlockLevel3;
|
||||
|
||||
/**
|
||||
* Something about temporal stability
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t anchor_pq;
|
||||
uint16_t anchor_power;
|
||||
} DoviExtMetadataBlockLevel4;
|
||||
|
||||
/**
|
||||
* Active area of the picture (letterbox, aspect ratio)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t active_area_left_offset;
|
||||
uint16_t active_area_right_offset;
|
||||
uint16_t active_area_top_offset;
|
||||
uint16_t active_area_bottom_offset;
|
||||
} DoviExtMetadataBlockLevel5;
|
||||
|
||||
/**
|
||||
* ST2086/HDR10 metadata fallback
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t max_display_mastering_luminance;
|
||||
uint16_t min_display_mastering_luminance;
|
||||
uint16_t max_content_light_level;
|
||||
uint16_t max_frame_average_light_level;
|
||||
} DoviExtMetadataBlockLevel6;
|
||||
|
||||
/**
|
||||
* Creative intent trim passes per target display peak brightness
|
||||
* For CM v4.0, L8 metadata only is present and used to compute L2
|
||||
*
|
||||
* This block can have varying byte lengths: 10, 12, 13, 19, 25
|
||||
* Depending on the length, the fields parsed default to zero and may not be set.
|
||||
* Up to (including):
|
||||
* - 10: ms_weight
|
||||
* - 12: target_mid_contrast
|
||||
* - 13: clip_trim
|
||||
* - 19: saturation_vector_field[0-5]
|
||||
* - 25: hue_vector_field[0-5]
|
||||
*/
|
||||
typedef struct {
|
||||
uint64_t length;
|
||||
uint8_t target_display_index;
|
||||
uint16_t trim_slope;
|
||||
uint16_t trim_offset;
|
||||
uint16_t trim_power;
|
||||
uint16_t trim_chroma_weight;
|
||||
uint16_t trim_saturation_gain;
|
||||
uint16_t ms_weight;
|
||||
uint16_t target_mid_contrast;
|
||||
uint16_t clip_trim;
|
||||
uint8_t saturation_vector_field0;
|
||||
uint8_t saturation_vector_field1;
|
||||
uint8_t saturation_vector_field2;
|
||||
uint8_t saturation_vector_field3;
|
||||
uint8_t saturation_vector_field4;
|
||||
uint8_t saturation_vector_field5;
|
||||
uint8_t hue_vector_field0;
|
||||
uint8_t hue_vector_field1;
|
||||
uint8_t hue_vector_field2;
|
||||
uint8_t hue_vector_field3;
|
||||
uint8_t hue_vector_field4;
|
||||
uint8_t hue_vector_field5;
|
||||
} DoviExtMetadataBlockLevel8;
|
||||
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of ExtMetadataBlockLevel8 structs
|
||||
*/
|
||||
const DoviExtMetadataBlockLevel8 *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviLevel8BlockList;
|
||||
|
||||
/**
|
||||
* Source/mastering display color primaries
|
||||
*
|
||||
* This block can have varying byte lengths: 1 or 17
|
||||
* Depending on the length, the fields parsed default to zero and may not be set.
|
||||
* Up to (including):
|
||||
* - 1: source_primary_index
|
||||
* - 17: source_primary_{red,green,blue,white}_{x,y}
|
||||
*/
|
||||
typedef struct {
|
||||
uint64_t length;
|
||||
uint8_t source_primary_index;
|
||||
uint16_t source_primary_red_x;
|
||||
uint16_t source_primary_red_y;
|
||||
uint16_t source_primary_green_x;
|
||||
uint16_t source_primary_green_y;
|
||||
uint16_t source_primary_blue_x;
|
||||
uint16_t source_primary_blue_y;
|
||||
uint16_t source_primary_white_x;
|
||||
uint16_t source_primary_white_y;
|
||||
} DoviExtMetadataBlockLevel9;
|
||||
|
||||
/**
|
||||
* Custom target display information
|
||||
*
|
||||
* This block can have varying byte lengths: 5 or 21
|
||||
* Depending on the length, the fields parsed default to zero and may not be set.
|
||||
* Up to (including):
|
||||
* - 5: target_primary_index
|
||||
* - 21: target_primary_{red,green,blue,white}_{x,y}
|
||||
*/
|
||||
typedef struct {
|
||||
uint64_t length;
|
||||
uint8_t target_display_index;
|
||||
uint16_t target_max_pq;
|
||||
uint16_t target_min_pq;
|
||||
uint8_t target_primary_index;
|
||||
uint16_t target_primary_red_x;
|
||||
uint16_t target_primary_red_y;
|
||||
uint16_t target_primary_green_x;
|
||||
uint16_t target_primary_green_y;
|
||||
uint16_t target_primary_blue_x;
|
||||
uint16_t target_primary_blue_y;
|
||||
uint16_t target_primary_white_x;
|
||||
uint16_t target_primary_white_y;
|
||||
} DoviExtMetadataBlockLevel10;
|
||||
|
||||
typedef struct {
|
||||
/**
|
||||
* Pointer to the list of ExtMetadataBlockLevel10 structs
|
||||
*/
|
||||
const DoviExtMetadataBlockLevel10 *const *list;
|
||||
/**
|
||||
* List length
|
||||
*/
|
||||
size_t len;
|
||||
} DoviLevel10BlockList;
|
||||
|
||||
/**
|
||||
* Content type metadata level
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t content_type;
|
||||
uint8_t whitepoint;
|
||||
bool reference_mode_flag;
|
||||
uint8_t reserved_byte2;
|
||||
uint8_t reserved_byte3;
|
||||
} DoviExtMetadataBlockLevel11;
|
||||
|
||||
/**
|
||||
* Metadata level present in CM v4.0
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t dm_mode;
|
||||
uint8_t dm_version_index;
|
||||
} DoviExtMetadataBlockLevel254;
|
||||
|
||||
/**
|
||||
* Metadata level optionally present in CM v2.9.
|
||||
* Different display modes (calibration/verify/bypass), debugging
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t dm_run_mode;
|
||||
uint8_t dm_run_version;
|
||||
uint8_t dm_debug0;
|
||||
uint8_t dm_debug1;
|
||||
uint8_t dm_debug2;
|
||||
uint8_t dm_debug3;
|
||||
} DoviExtMetadataBlockLevel255;
|
||||
|
||||
/**
|
||||
* C struct for the list of ext_metadata_block()
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Number of metadata blocks
|
||||
*/
|
||||
uint64_t num_ext_blocks;
|
||||
const DoviExtMetadataBlockLevel1 *level1;
|
||||
DoviLevel2BlockList level2;
|
||||
const DoviExtMetadataBlockLevel3 *level3;
|
||||
const DoviExtMetadataBlockLevel4 *level4;
|
||||
const DoviExtMetadataBlockLevel5 *level5;
|
||||
const DoviExtMetadataBlockLevel6 *level6;
|
||||
DoviLevel8BlockList level8;
|
||||
const DoviExtMetadataBlockLevel9 *level9;
|
||||
DoviLevel10BlockList level10;
|
||||
const DoviExtMetadataBlockLevel11 *level11;
|
||||
const DoviExtMetadataBlockLevel254 *level254;
|
||||
const DoviExtMetadataBlockLevel255 *level255;
|
||||
} DoviDmData;
|
||||
|
||||
/**
|
||||
* C struct for vdr_dm_data()
|
||||
*/
|
||||
typedef struct {
|
||||
bool compressed;
|
||||
uint64_t affected_dm_metadata_id;
|
||||
uint64_t current_dm_metadata_id;
|
||||
uint64_t scene_refresh_flag;
|
||||
int16_t ycc_to_rgb_coef0;
|
||||
int16_t ycc_to_rgb_coef1;
|
||||
int16_t ycc_to_rgb_coef2;
|
||||
int16_t ycc_to_rgb_coef3;
|
||||
int16_t ycc_to_rgb_coef4;
|
||||
int16_t ycc_to_rgb_coef5;
|
||||
int16_t ycc_to_rgb_coef6;
|
||||
int16_t ycc_to_rgb_coef7;
|
||||
int16_t ycc_to_rgb_coef8;
|
||||
uint32_t ycc_to_rgb_offset0;
|
||||
uint32_t ycc_to_rgb_offset1;
|
||||
uint32_t ycc_to_rgb_offset2;
|
||||
int16_t rgb_to_lms_coef0;
|
||||
int16_t rgb_to_lms_coef1;
|
||||
int16_t rgb_to_lms_coef2;
|
||||
int16_t rgb_to_lms_coef3;
|
||||
int16_t rgb_to_lms_coef4;
|
||||
int16_t rgb_to_lms_coef5;
|
||||
int16_t rgb_to_lms_coef6;
|
||||
int16_t rgb_to_lms_coef7;
|
||||
int16_t rgb_to_lms_coef8;
|
||||
uint16_t signal_eotf;
|
||||
uint16_t signal_eotf_param0;
|
||||
uint16_t signal_eotf_param1;
|
||||
uint32_t signal_eotf_param2;
|
||||
uint8_t signal_bit_depth;
|
||||
uint8_t signal_color_space;
|
||||
uint8_t signal_chroma_format;
|
||||
uint8_t signal_full_range_flag;
|
||||
uint16_t source_min_pq;
|
||||
uint16_t source_max_pq;
|
||||
uint16_t source_diagonal;
|
||||
DoviDmData dm_data;
|
||||
} DoviVdrDmData;
|
||||
|
||||
/**
|
||||
* Heap allocated list of valid RPU pointers
|
||||
*/
|
||||
typedef struct {
|
||||
DoviRpuOpaque *const *list;
|
||||
size_t len;
|
||||
const char *error;
|
||||
} DoviRpuOpaqueList;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif // __cplusplus
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the data must be valid.
|
||||
*
|
||||
* Parse a Dolby Vision RPU from unescaped byte buffer.
|
||||
* Adds an error if the parsing fails.
|
||||
*/
|
||||
DoviRpuOpaque *dovi_parse_rpu(const uint8_t *buf, size_t len);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the data must be valid.
|
||||
*
|
||||
* Parse a Dolby Vision from a AV1 ITU-T T.35 metadata OBU byte buffer.
|
||||
* Adds an error if the parsing fails.
|
||||
*/
|
||||
DoviRpuOpaque *dovi_parse_itu_t35_dovi_metadata_obu(const uint8_t *buf, size_t len);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the data must be valid.
|
||||
*
|
||||
* Parse a Dolby Vision from a (possibly) escaped HEVC UNSPEC 62 NAL unit byte buffer.
|
||||
* Adds an error if the parsing fails.
|
||||
*/
|
||||
DoviRpuOpaque *dovi_parse_unspec62_nalu(const uint8_t *buf, size_t len);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the opaque struct must be valid.
|
||||
* Avoid using on opaque pointers obtained through `dovi_parse_rpu_bin_file`.
|
||||
*
|
||||
* Free the RpuOpaque
|
||||
*/
|
||||
void dovi_rpu_free(DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the opaque struct must be valid.
|
||||
*
|
||||
* Get the last logged error for the RpuOpaque operations.
|
||||
*
|
||||
* On invalid parsing, an error is added.
|
||||
* The user should manually verify if there is an error, as the parsing does not return an error code.
|
||||
*/
|
||||
const char *dovi_rpu_get_error(const DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The data pointer should exist, and be allocated by Rust.
|
||||
*
|
||||
* Free a Data buffer
|
||||
*/
|
||||
void dovi_data_free(const DoviData *data);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Writes the encoded RPU as a byte buffer.
|
||||
* If an error occurs in the writing, it is logged to RpuOpaque.error
|
||||
*/
|
||||
const DoviData *dovi_write_rpu(DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Writes the encoded RPU, escapes the bytes for HEVC and prepends the buffer with 0x7C01.
|
||||
* If an error occurs in the writing, it is logged to RpuOpaque.error
|
||||
*/
|
||||
const DoviData *dovi_write_unspec62_nalu(DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
* The mode must be between 0 and 4.
|
||||
*
|
||||
* Converts the RPU to be compatible with a different Dolby Vision profile.
|
||||
* Possible modes:
|
||||
* - 0: Don't modify the RPU
|
||||
* - 1: Converts the RPU to be MEL compatible
|
||||
* - 2: Converts the RPU to be profile 8.1 compatible. Both luma and chroma mapping curves are set to no-op.
|
||||
* This mode handles source profiles 5, 7 and 8.
|
||||
* - 3: Converts to static profile 8.4
|
||||
* - 4: Converts to profile 8.1 preserving luma and chroma mapping. Old mode 2 behaviour.
|
||||
*
|
||||
* If an error occurs, it is logged to RpuOpaque.error.
|
||||
* Returns 0 if successful, -1 otherwise.
|
||||
*/
|
||||
int32_t dovi_convert_rpu_with_mode(DoviRpuOpaque *ptr,
|
||||
uint8_t mode);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the opaque struct must be valid.
|
||||
*
|
||||
* Get the DoVi RPU header struct.
|
||||
*/
|
||||
const DoviRpuDataHeader *dovi_rpu_get_header(const DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the struct must be valid.
|
||||
*
|
||||
* Frees the memory used by the RPU header.
|
||||
*/
|
||||
void dovi_rpu_free_header(const DoviRpuDataHeader *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the opaque struct must be valid.
|
||||
*
|
||||
* Get the DoVi RpuDataMapping struct.
|
||||
*/
|
||||
const DoviRpuDataMapping *dovi_rpu_get_data_mapping(const DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the struct must be valid.
|
||||
*
|
||||
* Frees the memory used by the RpuDataMapping.
|
||||
*/
|
||||
void dovi_rpu_free_data_mapping(const DoviRpuDataMapping *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the opaque struct must be valid.
|
||||
*
|
||||
* Get the DoVi VdrDmData struct.
|
||||
*/
|
||||
const DoviVdrDmData *dovi_rpu_get_vdr_dm_data(const DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the struct must be valid.
|
||||
*
|
||||
* Frees the memory used by the VdrDmData struct.
|
||||
*/
|
||||
void dovi_rpu_free_vdr_dm_data(const DoviVdrDmData *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the file path must be valid.
|
||||
*
|
||||
* Parses an existing RPU binary file.
|
||||
*
|
||||
* Returns the heap allocated `DoviRpuList` as a pointer.
|
||||
* The returned pointer may be null, or the list could be empty if an error occurred.
|
||||
*/
|
||||
const DoviRpuOpaqueList *dovi_parse_rpu_bin_file(const char *path);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The pointer to the struct must be valid.
|
||||
*
|
||||
* Frees the memory used by the DoviRpuOpaqueList struct.
|
||||
*/
|
||||
void dovi_rpu_list_free(const DoviRpuOpaqueList *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Sets the L5 metadata active area offsets.
|
||||
* If there is no L5 block present, it is created with the offsets.
|
||||
*/
|
||||
int32_t dovi_rpu_set_active_area_offsets(DoviRpuOpaque *ptr,
|
||||
uint16_t left,
|
||||
uint16_t right,
|
||||
uint16_t top,
|
||||
uint16_t bottom);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Converts the existing reshaping/mapping to become no-op.
|
||||
*/
|
||||
int32_t dovi_rpu_remove_mapping(DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Writes the encoded RPU as `itu_t_t35_payload_bytes` for AV1 ITU-T T.35 metadata OBU
|
||||
* If an error occurs in the writing, it is logged to RpuOpaque.error
|
||||
*/
|
||||
const DoviData *dovi_write_av1_rpu_metadata_obu_t35_payload(DoviRpuOpaque *ptr);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* The struct pointer must be valid.
|
||||
*
|
||||
* Writes the encoded RPU a complete AV1 `metadata_itut_t35()` OBU
|
||||
* If an error occurs in the writing, it is logged to RpuOpaque.error
|
||||
*/
|
||||
const DoviData *dovi_write_av1_rpu_metadata_obu_t35_complete(DoviRpuOpaque *ptr);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif /* DOVI_H */
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.media.MediaCodecInfo
|
||||
import android.media.MediaCodecList
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
|
||||
enum class DvConversionMode { DISABLED, DV81, HEVC_STRIP }
|
||||
|
||||
object DoviBridge {
|
||||
private const val TAG = "DoviBridge"
|
||||
|
||||
private val nativeLoaded: Boolean by lazy {
|
||||
try {
|
||||
System.loadLibrary("dovi_bridge")
|
||||
true
|
||||
} catch (_: UnsatisfiedLinkError) {
|
||||
Log.w(TAG, "Native lib not found")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun isAvailable(): Boolean = nativeLoaded &&
|
||||
runCatching { nativeIsConversionPathReady() }.getOrDefault(false)
|
||||
|
||||
/**
|
||||
* Check if the device has a hardware decoder that supports Dolby Vision Profile 7.
|
||||
* Queries MediaCodecList for decoders supporting video/dolby-vision with
|
||||
* DolbyVisionProfileDvheDtr (profile 7).
|
||||
*/
|
||||
val deviceSupportsDvProfile7: Boolean by lazy {
|
||||
try {
|
||||
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||
val supported = codecList.codecInfos.any { info ->
|
||||
!info.isEncoder && info.supportedTypes.any { type ->
|
||||
type.equals("video/dolby-vision", ignoreCase = true) &&
|
||||
info.getCapabilitiesForType(type).profileLevels.any { pl ->
|
||||
pl.profile == MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "Device DV Profile 7 support: $supported")
|
||||
supported
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to query DV7 support", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the device has a hardware decoder that supports Dolby Vision Profile 8
|
||||
* (DvheSt). DolbyVisionProfileDvheSt constant requires API 27+.
|
||||
*/
|
||||
val deviceSupportsDvProfile8: Boolean by lazy {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT < 27) {
|
||||
Log.i(TAG, "API < 27, cannot check DV Profile 8 support")
|
||||
return@lazy false
|
||||
}
|
||||
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||
val supported = codecList.codecInfos.any { info ->
|
||||
!info.isEncoder && info.supportedTypes.any { type ->
|
||||
type.equals("video/dolby-vision", ignoreCase = true) &&
|
||||
info.getCapabilitiesForType(type).profileLevels.any { pl ->
|
||||
pl.profile == MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "Device DV Profile 8 support: $supported")
|
||||
supported
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to query DV8 support", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun getConversionMode(): DvConversionMode = when {
|
||||
!isAvailable() -> DvConversionMode.DISABLED
|
||||
deviceSupportsDvProfile7 -> DvConversionMode.DISABLED // try native first; ExoPlayerCore retries with conversion on failure
|
||||
deviceSupportsDvProfile8 -> DvConversionMode.DV81
|
||||
else -> DvConversionMode.HEVC_STRIP
|
||||
}
|
||||
|
||||
/** Get the fallback mode when native DV7 decoding fails. */
|
||||
fun getDv7FallbackMode(): DvConversionMode = when {
|
||||
deviceSupportsDvProfile8 -> DvConversionMode.DV81
|
||||
else -> DvConversionMode.HEVC_STRIP
|
||||
}
|
||||
|
||||
fun convertRpuNalu(payload: ByteArray, mode: Int = 2): ByteArray? {
|
||||
if (!isAvailable() || payload.isEmpty()) return null
|
||||
return runCatching { nativeConvertDv7RpuToDv81(payload, mode) }
|
||||
.onFailure { Log.w(TAG, "RPU conversion failed: ${it.message}") }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
fun getVersion(): String? {
|
||||
if (!nativeLoaded) return null
|
||||
return runCatching { nativeGetBridgeVersion() }.getOrNull()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeConvertDv7RpuToDv81(payload: ByteArray, mode: Int): ByteArray?
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeIsConversionPathReady(): Boolean
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeGetBridgeVersion(): String
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.util.Log
|
||||
import androidx.media3.common.DataReader
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.ParsableByteArray
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/**
|
||||
* TrackOutput wrapper that processes DV Profile 7 HEVC samples based on conversion mode:
|
||||
*
|
||||
* - DV81: Convert RPU NALs via libdovi to Profile 8.1, present as video/dolby-vision
|
||||
* with dvhe.08.XX codec string. Preserves dynamic tone mapping metadata.
|
||||
* - HEVC_STRIP: Strip all DV enhancement layers, present as plain video/hevc.
|
||||
*
|
||||
* Two modes of NAL framing (auto-detected):
|
||||
* - Annex B (MKV path): MatroskaExtractor outputs 00 00 00 01 start codes
|
||||
* - Length-prefixed (MP4 path): Mp4Extractor outputs 4-byte big-endian lengths
|
||||
*
|
||||
* NAL processing:
|
||||
* - Type 62 (UNSPEC62): DV RPU → convert (DV81) or strip (HEVC_STRIP)
|
||||
* - Type 63 (UNSPEC63): DV Enhancement Layer → strip
|
||||
* - nuh_layer_id > 0: Enhancement layer NAL → strip
|
||||
* - All retained NALs: normalize nuh_layer_id to 0
|
||||
*/
|
||||
class DoviConvertingTrackOutput(
|
||||
private val delegate: TrackOutput,
|
||||
private val dvMode: DvConversionMode = DvConversionMode.HEVC_STRIP,
|
||||
) : TrackOutput {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DoviConvertTrack"
|
||||
private const val NAL_TYPE_UNSPEC62 = 62
|
||||
private const val NAL_TYPE_UNSPEC63 = 63
|
||||
private const val LIBDOVI_MODE_TO_81 = 2
|
||||
private val ANNEX_B_START_CODE = byteArrayOf(0, 0, 0, 1)
|
||||
}
|
||||
|
||||
var conversionActive = false
|
||||
private set
|
||||
var strippedNalCount = 0L
|
||||
private set
|
||||
var convertedRpuCount = 0L
|
||||
private set
|
||||
|
||||
// Sample buffering between sampleData() and sampleMetadata()
|
||||
private val sampleBuffer = ByteArrayOutputStream(256 * 1024)
|
||||
private var buffering = false
|
||||
|
||||
override fun format(format: Format) {
|
||||
if (!conversionActive) {
|
||||
val codecs = format.codecs
|
||||
if (codecs != null && codecs.startsWith("dvhe.07")) {
|
||||
conversionActive = true
|
||||
Log.i(TAG, "DV Profile 7 detected ($codecs), mode=$dvMode")
|
||||
Log.i(TAG, "Original format: mime=${format.sampleMimeType}, codecs=$codecs, " +
|
||||
"initData=${format.initializationData.size} entries " +
|
||||
"(${format.initializationData.mapIndexed { i, d -> "$i:${d.size}B" }.joinToString()})")
|
||||
|
||||
val newFormat = when (dvMode) {
|
||||
DvConversionMode.DV81 -> {
|
||||
// Parse DV level from codec string: "dvhe.07.06" → 6
|
||||
val level = codecs.split('.').getOrNull(2)?.toIntOrNull() ?: 6
|
||||
val newCodecs = "dvhe.08.%02d".format(level)
|
||||
val dvConfigRecord = buildDv81ConfigRecord(level)
|
||||
Log.i(TAG, "DV81: rewriting to $newCodecs, config=${dvConfigRecord.size}B")
|
||||
|
||||
format.buildUpon()
|
||||
.setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION)
|
||||
.setCodecs(newCodecs)
|
||||
.setInitializationData(
|
||||
if (format.initializationData.isNotEmpty())
|
||||
listOf(format.initializationData[0], dvConfigRecord)
|
||||
else
|
||||
listOf(ByteArray(0), dvConfigRecord)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
else -> {
|
||||
// HEVC_STRIP: present as plain HEVC
|
||||
Log.i(TAG, "HEVC_STRIP: rewriting to video/hevc")
|
||||
format.buildUpon()
|
||||
.setSampleMimeType(MimeTypes.VIDEO_H265)
|
||||
.setCodecs(null)
|
||||
.setInitializationData(
|
||||
if (format.initializationData.isNotEmpty())
|
||||
listOf(format.initializationData[0])
|
||||
else
|
||||
emptyList()
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Rewritten format: mime=${newFormat.sampleMimeType}, " +
|
||||
"codecs=${newFormat.codecs}, initData=${newFormat.initializationData.size} entries")
|
||||
delegate.format(newFormat)
|
||||
return
|
||||
}
|
||||
}
|
||||
delegate.format(format)
|
||||
}
|
||||
|
||||
override fun sampleData(
|
||||
input: DataReader, length: Int, allowEndOfInput: Boolean, sampleDataPart: Int
|
||||
): Int {
|
||||
if (!conversionActive) {
|
||||
return delegate.sampleData(input, length, allowEndOfInput, sampleDataPart)
|
||||
}
|
||||
|
||||
// Buffer sample data for processing at sampleMetadata() time
|
||||
buffering = true
|
||||
val buf = ByteArray(length)
|
||||
val bytesRead = input.read(buf, 0, length)
|
||||
if (bytesRead > 0) {
|
||||
sampleBuffer.write(buf, 0, bytesRead)
|
||||
}
|
||||
return bytesRead
|
||||
}
|
||||
|
||||
override fun sampleData(data: ParsableByteArray, length: Int, sampleDataPart: Int) {
|
||||
if (!conversionActive) {
|
||||
delegate.sampleData(data, length, sampleDataPart)
|
||||
return
|
||||
}
|
||||
|
||||
// Buffer sample data for processing at sampleMetadata() time
|
||||
buffering = true
|
||||
val bytes = ByteArray(length)
|
||||
data.readBytes(bytes, 0, length)
|
||||
sampleBuffer.write(bytes, 0, length)
|
||||
}
|
||||
|
||||
override fun sampleMetadata(
|
||||
timeUs: Long, flags: Int, size: Int, offset: Int, cryptoData: TrackOutput.CryptoData?
|
||||
) {
|
||||
if (!conversionActive || !buffering) {
|
||||
delegate.sampleMetadata(timeUs, flags, size, offset, cryptoData)
|
||||
return
|
||||
}
|
||||
|
||||
buffering = false
|
||||
val rawSample = sampleBuffer.toByteArray()
|
||||
sampleBuffer.reset()
|
||||
|
||||
val processed = try {
|
||||
processNalUnits(rawSample)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "NAL processing failed, passing raw sample", e)
|
||||
rawSample
|
||||
}
|
||||
|
||||
// Skip empty samples (all NALs were DV layers) — don't confuse the decoder
|
||||
if (processed.isEmpty()) return
|
||||
|
||||
// Write processed data to delegate. Offset must be 0 since we write exactly
|
||||
// the processed amount (no trailing data from next sample in the buffer).
|
||||
val parsable = ParsableByteArray(processed, processed.size)
|
||||
delegate.sampleData(parsable, processed.size, TrackOutput.SAMPLE_DATA_PART_MAIN)
|
||||
delegate.sampleMetadata(timeUs, flags, processed.size, 0, cryptoData)
|
||||
}
|
||||
|
||||
// Sample counter for periodic logging
|
||||
private var sampleCount = 0L
|
||||
|
||||
/**
|
||||
* Process NAL units in the sample data. Auto-detects format:
|
||||
* - Annex B (00 00 00 01 / 00 00 01 start codes) — used by MatroskaExtractor
|
||||
* - Length-prefixed (4-byte big-endian length) — used by Mp4Extractor
|
||||
*
|
||||
* Strips UNSPEC62 RPU NALs, UNSPEC63 EL NALs, and any NAL with nuh_layer_id > 0.
|
||||
* Normalizes nuh_layer_id to 0 on all retained NALs.
|
||||
* Output uses the same format as input.
|
||||
*/
|
||||
private fun processNalUnits(data: ByteArray): ByteArray {
|
||||
if (data.size < 4) return data
|
||||
|
||||
// Auto-detect: Annex B starts with 00 00 00 01 or 00 00 01
|
||||
val isAnnexB = (data.size >= 4 && data[0] == 0.toByte() && data[1] == 0.toByte() &&
|
||||
data[2] == 0.toByte() && data[3] == 1.toByte()) ||
|
||||
(data.size >= 3 && data[0] == 0.toByte() && data[1] == 0.toByte() &&
|
||||
data[2] == 1.toByte())
|
||||
|
||||
if (sampleCount == 0L) {
|
||||
Log.d(TAG, "NAL format detected: ${if (isAnnexB) "Annex B" else "length-prefixed"}, " +
|
||||
"first bytes: ${data.take(8).joinToString(" ") { "%02X".format(it) }}")
|
||||
}
|
||||
|
||||
return if (isAnnexB) processAnnexBNals(data) else processLengthPrefixedNals(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all Annex B start code positions (00 00 01 or 00 00 00 01) in the data.
|
||||
* Returns list of pairs: (startCodeEnd, startCodeLen) where startCodeEnd is the
|
||||
* byte index right after the start code, and startCodeLen is 3 or 4.
|
||||
*/
|
||||
private fun findAnnexBStartCodes(data: ByteArray): List<Pair<Int, Int>> {
|
||||
val positions = mutableListOf<Pair<Int, Int>>()
|
||||
var i = 0
|
||||
while (i < data.size - 2) {
|
||||
if (data[i] == 0.toByte() && data[i + 1] == 0.toByte()) {
|
||||
if (i + 3 < data.size && data[i + 2] == 0.toByte() && data[i + 3] == 1.toByte()) {
|
||||
// 4-byte start code: 00 00 00 01
|
||||
positions.add(Pair(i + 4, 4))
|
||||
i += 4
|
||||
continue
|
||||
} else if (data[i + 2] == 1.toByte()) {
|
||||
// 3-byte start code: 00 00 01
|
||||
positions.add(Pair(i + 3, 3))
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
return positions
|
||||
}
|
||||
|
||||
/** Process Annex B formatted NAL units (MKV path). */
|
||||
private fun processAnnexBNals(data: ByteArray): ByteArray {
|
||||
val output = ByteArrayOutputStream(data.size)
|
||||
var kept = 0
|
||||
var stripped = 0
|
||||
|
||||
val startCodes = findAnnexBStartCodes(data)
|
||||
if (startCodes.isEmpty()) {
|
||||
sampleCount++
|
||||
return data // No start codes found, pass through
|
||||
}
|
||||
|
||||
for (idx in startCodes.indices) {
|
||||
val nalStart = startCodes[idx].first
|
||||
val nalEnd = if (idx + 1 < startCodes.size) {
|
||||
// NAL ends where next start code begins (subtract its start code length area)
|
||||
// Find the start of the next start code pattern
|
||||
startCodes[idx + 1].first - startCodes[idx + 1].second
|
||||
} else {
|
||||
data.size
|
||||
}
|
||||
|
||||
if (nalEnd <= nalStart) continue
|
||||
val nalData = data.copyOfRange(nalStart, nalEnd)
|
||||
|
||||
if (nalData.size >= 2) {
|
||||
val nalType = (nalData[0].toInt() ushr 1) and 0x3F
|
||||
val nuhLayerId = ((nalData[0].toInt() and 1) shl 5) or
|
||||
((nalData[1].toInt() ushr 3) and 0x1F)
|
||||
|
||||
when {
|
||||
nalType == NAL_TYPE_UNSPEC62 && dvMode == DvConversionMode.DV81 -> {
|
||||
// Convert RPU NAL via libdovi instead of stripping
|
||||
val converted = DoviBridge.convertRpuNalu(nalData, LIBDOVI_MODE_TO_81)
|
||||
if (converted != null) {
|
||||
normalizeLayerId(converted)
|
||||
output.write(ANNEX_B_START_CODE)
|
||||
output.write(converted)
|
||||
convertedRpuCount++
|
||||
kept++
|
||||
} else {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
}
|
||||
nalType == NAL_TYPE_UNSPEC62 || nalType == NAL_TYPE_UNSPEC63 || nuhLayerId > 0 -> {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
else -> {
|
||||
normalizeLayerId(nalData)
|
||||
// Write with 4-byte start code (consistent output)
|
||||
output.write(ANNEX_B_START_CODE)
|
||||
output.write(nalData)
|
||||
kept++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output.write(ANNEX_B_START_CODE)
|
||||
output.write(nalData)
|
||||
kept++
|
||||
}
|
||||
}
|
||||
|
||||
sampleCount++
|
||||
if (sampleCount <= 3 || (sampleCount % 500 == 0L)) {
|
||||
Log.d(TAG, "Sample #$sampleCount (AnnexB): ${data.size}B -> ${output.size()}B, " +
|
||||
"kept=$kept stripped=$stripped NALs")
|
||||
}
|
||||
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
/** Process length-prefixed NAL units (MP4 path). */
|
||||
private fun processLengthPrefixedNals(data: ByteArray): ByteArray {
|
||||
val output = ByteArrayOutputStream(data.size)
|
||||
var pos = 0
|
||||
var kept = 0
|
||||
var stripped = 0
|
||||
|
||||
while (pos + 4 <= data.size) {
|
||||
val nalLen = ((data[pos].toInt() and 0xFF) shl 24) or
|
||||
((data[pos + 1].toInt() and 0xFF) shl 16) or
|
||||
((data[pos + 2].toInt() and 0xFF) shl 8) or
|
||||
(data[pos + 3].toInt() and 0xFF)
|
||||
|
||||
if (nalLen <= 0 || pos + 4 + nalLen > data.size) {
|
||||
if (sampleCount < 5) {
|
||||
Log.w(TAG, "Bad NAL length $nalLen at pos $pos (data.size=${data.size})")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
val nalStart = pos + 4
|
||||
val nalData = data.copyOfRange(nalStart, nalStart + nalLen)
|
||||
|
||||
if (nalData.size >= 2) {
|
||||
val nalType = (nalData[0].toInt() ushr 1) and 0x3F
|
||||
val nuhLayerId = ((nalData[0].toInt() and 1) shl 5) or
|
||||
((nalData[1].toInt() ushr 3) and 0x1F)
|
||||
|
||||
when {
|
||||
nalType == NAL_TYPE_UNSPEC62 && dvMode == DvConversionMode.DV81 -> {
|
||||
// Convert RPU NAL via libdovi instead of stripping
|
||||
val converted = DoviBridge.convertRpuNalu(nalData, LIBDOVI_MODE_TO_81)
|
||||
if (converted != null) {
|
||||
normalizeLayerId(converted)
|
||||
writeLengthPrefixedNal(output, converted)
|
||||
convertedRpuCount++
|
||||
kept++
|
||||
} else {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
}
|
||||
nalType == NAL_TYPE_UNSPEC62 || nalType == NAL_TYPE_UNSPEC63 || nuhLayerId > 0 -> {
|
||||
strippedNalCount++
|
||||
stripped++
|
||||
}
|
||||
else -> {
|
||||
normalizeLayerId(nalData)
|
||||
writeLengthPrefixedNal(output, nalData)
|
||||
kept++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
writeLengthPrefixedNal(output, nalData)
|
||||
kept++
|
||||
}
|
||||
|
||||
pos += 4 + nalLen
|
||||
}
|
||||
|
||||
sampleCount++
|
||||
if (sampleCount <= 3 || (sampleCount % 500 == 0L)) {
|
||||
Log.d(TAG, "Sample #$sampleCount (LenPrefix): ${data.size}B -> ${output.size()}B, " +
|
||||
"kept=$kept stripped=$stripped NALs")
|
||||
}
|
||||
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
private fun normalizeLayerId(nalData: ByteArray) {
|
||||
if (nalData.size >= 2) {
|
||||
nalData[0] = (nalData[0].toInt() and 0xFE).toByte() // Clear bit 0 of byte 0
|
||||
nalData[1] = (nalData[1].toInt() and 0x07).toByte() // Clear bits 7-3 of byte 1
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeLengthPrefixedNal(output: ByteArrayOutputStream, nalData: ByteArray) {
|
||||
val len = nalData.size
|
||||
output.write((len ushr 24) and 0xFF)
|
||||
output.write((len ushr 16) and 0xFF)
|
||||
output.write((len ushr 8) and 0xFF)
|
||||
output.write(len and 0xFF)
|
||||
output.write(nalData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a 24-byte DOVIDecoderConfigurationRecord for DV Profile 8.1.
|
||||
*
|
||||
* Binary layout (from Dolby Vision spec):
|
||||
* byte[0]: dv_version_major = 1
|
||||
* byte[1]: dv_version_minor = 0
|
||||
* byte[2]: dv_profile (7 bits) | dv_level MSB (1 bit)
|
||||
* byte[3]: dv_level low 5 bits (5 bits) | rpu_present (1) | el_present (1) | bl_present (1)
|
||||
* byte[4]: bl_compatibility_id (4 bits) | md_compression (2 bits) | reserved (2 bits)
|
||||
* byte[5-23]: reserved (zeros)
|
||||
*/
|
||||
private fun buildDv81ConfigRecord(level: Int): ByteArray {
|
||||
val record = ByteArray(24)
|
||||
record[0] = 0x01 // dv_version_major = 1
|
||||
record[1] = 0x00 // dv_version_minor = 0
|
||||
record[2] = ((8 shl 1) or ((level ushr 5) and 0x01)).toByte() // profile=8 | level MSB
|
||||
record[3] = (((level and 0x1F) shl 3) or 0x05).toByte() // level low 5 | rpu=1 el=0 bl=1
|
||||
record[4] = (1 shl 4).toByte() // bl_compatibility_id=1 (HDR10)
|
||||
// bytes 5-23 remain 0 (reserved)
|
||||
return record
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.extractor.Extractor
|
||||
import androidx.media3.extractor.ExtractorInput
|
||||
import androidx.media3.extractor.ExtractorOutput
|
||||
import androidx.media3.extractor.PositionHolder
|
||||
import androidx.media3.extractor.SeekMap
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
|
||||
/**
|
||||
* Extractor decorator for Mp4/FragmentedMp4 containers.
|
||||
* Wraps the video TrackOutput with DoviConvertingTrackOutput to perform
|
||||
* DV Profile 7 → 8.1 conversion via inline NAL processing.
|
||||
*
|
||||
* For MP4, RPU (UNSPEC62) and EL (UNSPEC63) NALs are interleaved in sample data,
|
||||
* so no BlockAdditions handling is needed.
|
||||
*/
|
||||
class DoviExtractorWrapper(
|
||||
private val delegate: Extractor,
|
||||
private val dvMode: DvConversionMode = DvConversionMode.HEVC_STRIP,
|
||||
) : Extractor {
|
||||
|
||||
@Volatile var doviTrackOutput: DoviConvertingTrackOutput? = null
|
||||
private set
|
||||
|
||||
override fun sniff(input: ExtractorInput): Boolean = delegate.sniff(input)
|
||||
|
||||
override fun init(output: ExtractorOutput) {
|
||||
delegate.init(object : ExtractorOutput {
|
||||
override fun track(id: Int, type: Int): TrackOutput {
|
||||
val original = output.track(id, type)
|
||||
if (type == C.TRACK_TYPE_VIDEO) {
|
||||
val wrapper = DoviConvertingTrackOutput(original, dvMode)
|
||||
doviTrackOutput = wrapper
|
||||
return wrapper
|
||||
}
|
||||
return original
|
||||
}
|
||||
|
||||
override fun endTracks() = output.endTracks()
|
||||
override fun seekMap(seekMap: SeekMap) = output.seekMap(seekMap)
|
||||
})
|
||||
}
|
||||
|
||||
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int =
|
||||
delegate.read(input, seekPosition)
|
||||
|
||||
override fun seek(position: Long, timeUs: Long) = delegate.seek(position, timeUs)
|
||||
|
||||
override fun release() = delegate.release()
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.util.Log
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.extractor.ExtractorInput
|
||||
import androidx.media3.extractor.ExtractorOutput
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
import androidx.media3.extractor.mkv.MatroskaExtractor
|
||||
import androidx.media3.extractor.text.SubtitleParser
|
||||
import io.github.peerless2012.ass.media.AssHandler
|
||||
|
||||
/**
|
||||
* MatroskaExtractor subclass that adds:
|
||||
* 1. ASS subtitle support (font extraction from MKV attachments, video size reporting)
|
||||
* 2. Dolby Vision Profile 7 → 8.1 conversion (RPU capture from BlockAdditions)
|
||||
*
|
||||
* Replaces both AssMatroskaExtractor and adds DV handling on top.
|
||||
* Font extraction and video size reporting are replicated from AssMatroskaExtractor
|
||||
* since that class is final and cannot be extended.
|
||||
*/
|
||||
class DoviMatroskaExtractor(
|
||||
subtitleParserFactory: SubtitleParser.Factory,
|
||||
private val assHandler: AssHandler,
|
||||
private val dvMode: DvConversionMode = DvConversionMode.HEVC_STRIP,
|
||||
) : MatroskaExtractor(subtitleParserFactory) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DoviMkvExtractor"
|
||||
|
||||
// Matroska element IDs for attachments
|
||||
private const val ID_ATTACHMENTS = 0x1941A469
|
||||
private const val ID_ATTACHED_FILE = 0x61A7
|
||||
private const val ID_FILE_NAME = 0x466E
|
||||
private const val ID_FILE_MIME_TYPE = 0x4660
|
||||
private const val ID_FILE_DATA = 0x465C
|
||||
|
||||
// Track video element
|
||||
private const val ID_VIDEO = 0xE0
|
||||
|
||||
// EBML header - hook point for output wrapping
|
||||
private const val ID_EBML = 0x1A45DFA3
|
||||
|
||||
// MKV element type constants (matching MatroskaExtractor internals)
|
||||
private const val TYPE_MASTER = 1
|
||||
private const val TYPE_STRING = 3
|
||||
private const val TYPE_BINARY = 4
|
||||
|
||||
private val FONT_MIME_TYPES = setOf(
|
||||
"application/x-truetype-font",
|
||||
"application/x-font-truetype",
|
||||
"application/vnd.ms-opentype",
|
||||
"font/sfnt",
|
||||
"font/ttf",
|
||||
"font/otf",
|
||||
"font/collection",
|
||||
)
|
||||
|
||||
private val extractorOutputField by lazy {
|
||||
try {
|
||||
MatroskaExtractor::class.java.getDeclaredField("extractorOutput").apply {
|
||||
isAccessible = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Cannot access extractorOutput field: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ASS font attachment state
|
||||
private var currentAttachmentName: String? = null
|
||||
private var currentAttachmentMime: String? = null
|
||||
|
||||
// DV conversion state
|
||||
@Volatile var doviTrackOutput: DoviConvertingTrackOutput? = null
|
||||
internal set
|
||||
|
||||
// ===== Matroska element overrides =====
|
||||
|
||||
override fun getElementType(id: Int): Int = when (id) {
|
||||
ID_ATTACHMENTS, ID_ATTACHED_FILE -> TYPE_MASTER
|
||||
ID_FILE_NAME, ID_FILE_MIME_TYPE -> TYPE_STRING
|
||||
ID_FILE_DATA -> TYPE_BINARY
|
||||
else -> super.getElementType(id)
|
||||
}
|
||||
|
||||
override fun isLevel1Element(id: Int): Boolean =
|
||||
super.isLevel1Element(id) || id == ID_ATTACHMENTS
|
||||
|
||||
override fun startMasterElement(id: Int, contentPosition: Long, contentSize: Long) {
|
||||
when (id) {
|
||||
ID_EBML -> {
|
||||
wrapExtractorOutput()
|
||||
super.startMasterElement(id, contentPosition, contentSize)
|
||||
}
|
||||
ID_ATTACHED_FILE -> clearAttachment()
|
||||
else -> super.startMasterElement(id, contentPosition, contentSize)
|
||||
}
|
||||
}
|
||||
|
||||
override fun endMasterElement(id: Int) {
|
||||
when (id) {
|
||||
ID_VIDEO -> {
|
||||
val track = getCurrentTrack(id)
|
||||
assHandler.setVideoSize(track.width, track.height)
|
||||
super.endMasterElement(id)
|
||||
}
|
||||
ID_ATTACHED_FILE -> clearAttachment()
|
||||
else -> super.endMasterElement(id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stringElement(id: Int, value: String) {
|
||||
when (id) {
|
||||
ID_FILE_NAME -> currentAttachmentName = value
|
||||
ID_FILE_MIME_TYPE -> currentAttachmentMime = value
|
||||
else -> super.stringElement(id, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun binaryElement(id: Int, size: Int, input: ExtractorInput) {
|
||||
if (id == ID_FILE_DATA) {
|
||||
val mime = currentAttachmentMime
|
||||
val name = currentAttachmentName
|
||||
if (mime != null && name != null && mime in FONT_MIME_TYPES) {
|
||||
val data = ByteArray(size)
|
||||
input.readFully(data, 0, size)
|
||||
assHandler.addFont(name, data)
|
||||
} else {
|
||||
input.skipFully(size)
|
||||
}
|
||||
clearAttachment()
|
||||
} else {
|
||||
super.binaryElement(id, size, input)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== ExtractorOutput wrapping =====
|
||||
|
||||
private fun wrapExtractorOutput() {
|
||||
val field = extractorOutputField ?: return
|
||||
val output = field.get(this) as? ExtractorOutput ?: return
|
||||
if (output is DoviExtractorOutputWrapper) return
|
||||
field.set(this, DoviExtractorOutputWrapper(output, this))
|
||||
}
|
||||
|
||||
private fun clearAttachment() {
|
||||
currentAttachmentName = null
|
||||
currentAttachmentMime = null
|
||||
}
|
||||
|
||||
/**
|
||||
* ExtractorOutput wrapper that intercepts video track creation
|
||||
* to insert DoviConvertingTrackOutput for DV processing.
|
||||
*/
|
||||
class DoviExtractorOutputWrapper(
|
||||
private val delegate: ExtractorOutput,
|
||||
private val extractor: DoviMatroskaExtractor,
|
||||
) : ExtractorOutput {
|
||||
override fun track(id: Int, type: Int): TrackOutput {
|
||||
val original = delegate.track(id, type)
|
||||
if (type == C.TRACK_TYPE_VIDEO) {
|
||||
val wrapper = DoviConvertingTrackOutput(original, extractor.dvMode)
|
||||
extractor.doviTrackOutput = wrapper
|
||||
return wrapper
|
||||
}
|
||||
return original
|
||||
}
|
||||
|
||||
override fun endTracks() = delegate.endTracks()
|
||||
override fun seekMap(seekMap: androidx.media3.extractor.SeekMap) = delegate.seekMap(seekMap)
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import androidx.media3.extractor.DefaultExtractorsFactory
|
||||
import androidx.media3.extractor.mp4.FragmentedMp4Extractor
|
||||
import androidx.media3.extractor.mp4.Mp4Extractor
|
||||
import androidx.media3.extractor.mkv.MatroskaExtractor
|
||||
import androidx.media3.ui.CaptionStyleCompat
|
||||
import androidx.media3.ui.SubtitleView
|
||||
@@ -191,6 +193,12 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
// DV conversion state
|
||||
private var dvMode: DvConversionMode = DvConversionMode.DISABLED
|
||||
private var dv7RetryAttempted = false
|
||||
@Volatile private var activeDoviMkvExtractor: DoviMatroskaExtractor? = null
|
||||
@Volatile private var activeDoviMp4Wrapper: DoviExtractorWrapper? = null
|
||||
|
||||
fun initialize(bufferSizeBytes: Int? = null, tunnelingEnabled: Boolean = true): Boolean {
|
||||
if (isInitialized) {
|
||||
Log.d(TAG, "Already initialized")
|
||||
@@ -198,6 +206,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
}
|
||||
|
||||
tunnelingUserEnabled = tunnelingEnabled
|
||||
this.dvMode = DoviBridge.getConversionMode()
|
||||
Log.i(TAG, "DV conversion: mode=$dvMode, bridge=${DoviBridge.isAvailable()}, " +
|
||||
"deviceDV7=${DoviBridge.deviceSupportsDvProfile7}, deviceDV8=${DoviBridge.deviceSupportsDvProfile8}")
|
||||
disposing = false
|
||||
|
||||
try {
|
||||
@@ -331,13 +342,30 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
|
||||
val assParserFactory = AssSubtitleParserFactory(handler)
|
||||
|
||||
// Wrap extractors to replace MatroskaExtractor with ASS-aware variant
|
||||
// Wrap extractors: replace MatroskaExtractor with ASS+DV variant,
|
||||
// wrap MP4 extractors with DV converter when enabled.
|
||||
// Reads this.dvMode each time (not captured) so DV7→8.1 retry can
|
||||
// change mode and reload without reinitializing the player.
|
||||
val wrappedExtractorsFactory = androidx.media3.extractor.ExtractorsFactory {
|
||||
val currentDvMode = this.dvMode
|
||||
val doviEnabled = currentDvMode != DvConversionMode.DISABLED
|
||||
extractorsFactory.createExtractors().map { extractor ->
|
||||
if (extractor is androidx.media3.extractor.mkv.MatroskaExtractor) {
|
||||
AssMatroskaExtractor(assParserFactory, handler)
|
||||
} else {
|
||||
extractor
|
||||
when {
|
||||
extractor is MatroskaExtractor -> {
|
||||
if (doviEnabled) {
|
||||
DoviMatroskaExtractor(assParserFactory, handler, currentDvMode).also {
|
||||
activeDoviMkvExtractor = it
|
||||
}
|
||||
} else {
|
||||
AssMatroskaExtractor(assParserFactory, handler)
|
||||
}
|
||||
}
|
||||
doviEnabled && (extractor is Mp4Extractor || extractor is FragmentedMp4Extractor) -> {
|
||||
DoviExtractorWrapper(extractor, currentDvMode).also {
|
||||
activeDoviMp4Wrapper = it
|
||||
}
|
||||
}
|
||||
else -> extractor
|
||||
}
|
||||
}.toTypedArray()
|
||||
}
|
||||
@@ -553,6 +581,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
val hasAnyVideoGroup = tracks.groups.any { it.type == C.TRACK_TYPE_VIDEO }
|
||||
val hasSelectedVideo = tracks.groups.any { it.type == C.TRACK_TYPE_VIDEO && it.isSelected }
|
||||
if (hasAnyVideoGroup && !hasSelectedVideo && currentMediaUri != null) {
|
||||
// Try DV conversion before falling to MPV
|
||||
if (retryWithDvConversion("video track not selected")) return
|
||||
emitLog("warn", "fallback", "Video track present but not selected (unsupported codec)")
|
||||
delegate?.onFormatUnsupported(
|
||||
uri = currentMediaUri!!,
|
||||
@@ -573,6 +603,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
stopFrameWatchdog()
|
||||
cancelDecoderHangCheck()
|
||||
|
||||
// If native DV7 failed, retry with conversion before falling to MPV
|
||||
if (error.errorCode in 4001..4005 && retryWithDvConversion("decoder error ${error.errorCode}")) return
|
||||
|
||||
if (currentMediaUri != null) {
|
||||
Log.w(TAG, "ExoPlayer error (code ${error.errorCode}) - attempting fallback to MPV")
|
||||
val handled = delegate?.onFormatUnsupported(
|
||||
@@ -591,6 +624,32 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* When native DV7 decoding fails (device falsely advertises DV7 support),
|
||||
* upgrade to DV7→8.1 conversion or HEVC strip and reload the media.
|
||||
* Returns true if retry was initiated.
|
||||
*/
|
||||
private fun retryWithDvConversion(reason: String): Boolean {
|
||||
if (dv7RetryAttempted) return false
|
||||
if (dvMode != DvConversionMode.DISABLED) return false
|
||||
if (!DoviBridge.isAvailable()) return false
|
||||
val uri = currentMediaUri ?: return false
|
||||
|
||||
dv7RetryAttempted = true
|
||||
val newMode = DoviBridge.getDv7FallbackMode()
|
||||
dvMode = newMode
|
||||
Log.i(TAG, "Native DV7 playback failed ($reason), retrying with $newMode")
|
||||
emitLog("info", "dv-fallback", "DV7 native failed ($reason), retrying as $newMode")
|
||||
|
||||
open(
|
||||
uri = uri,
|
||||
headers = currentHeaders,
|
||||
startPositionMs = lastPosition,
|
||||
autoPlay = true,
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
Log.d(TAG, "onMediaItemTransition: ${mediaItem?.mediaId}, reason: $reason")
|
||||
delegate?.onEvent("file-loaded", null)
|
||||
@@ -1018,6 +1077,11 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
stopFrameWatchdog()
|
||||
cancelDecoderHangCheck()
|
||||
|
||||
// Reset DV7 retry flag when opening a different file
|
||||
if (uri != currentMediaUri) {
|
||||
dv7RetryAttempted = false
|
||||
}
|
||||
|
||||
currentMediaUri = uri
|
||||
currentHeaders = headers
|
||||
externalSubtitles.clear()
|
||||
@@ -1387,6 +1451,14 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
"playbackSpeed" to player.playbackParameters.speed,
|
||||
"isPlaying" to player.isPlaying,
|
||||
"playbackState" to player.playbackState,
|
||||
// DV conversion (query extractor's track output, which is set during extraction)
|
||||
"dvConversionActive" to ((activeDoviMkvExtractor?.doviTrackOutput?.conversionActive
|
||||
?: activeDoviMp4Wrapper?.doviTrackOutput?.conversionActive) == true),
|
||||
"dvConversionMode" to dvMode.name,
|
||||
"dvStrippedNals" to ((activeDoviMkvExtractor?.doviTrackOutput?.strippedNalCount
|
||||
?: activeDoviMp4Wrapper?.doviTrackOutput?.strippedNalCount) ?: 0L),
|
||||
"dvConvertedRpus" to ((activeDoviMkvExtractor?.doviTrackOutput?.convertedRpuCount
|
||||
?: activeDoviMp4Wrapper?.doviTrackOutput?.convertedRpuCount) ?: 0L),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
private val nameToId = mutableMapOf<String, Int>()
|
||||
private var configuredBufferSizeBytes: Int? = null
|
||||
private var configuredTunnelingEnabled: Boolean = true
|
||||
|
||||
private var debugLoggingEnabled: Boolean = false
|
||||
|
||||
// FlutterPlugin
|
||||
|
||||
@@ -73,6 +73,8 @@ class _PlayerPerformanceOverlayState extends State<PlayerPerformanceOverlay> {
|
||||
_metric('Decoder', _stats.hwdecFormatted),
|
||||
if (_stats.aspectName != null && _stats.aspectName!.isNotEmpty) _metric('Aspect', _stats.aspectName!),
|
||||
if (_stats.rotate != null && _stats.rotate != 0) _metric('Rotation', _stats.rotateFormatted),
|
||||
if (_stats.dvConversionActive)
|
||||
_metric('DV', _stats.dvConversionMode == 'DV81' ? '7→8.1' : '7→HEVC'),
|
||||
]),
|
||||
// Color section - MPV only (ExoPlayer doesn't provide this info)
|
||||
if (isMpv) ...[
|
||||
|
||||
@@ -48,6 +48,10 @@ class PerformanceStats {
|
||||
final double? cacheSpeed;
|
||||
final double? cacheDuration;
|
||||
|
||||
// DV conversion
|
||||
final bool dvConversionActive;
|
||||
final String dvConversionMode; // "DV81", "HEVC_STRIP", "DISABLED"
|
||||
|
||||
// App metrics
|
||||
final int? appMemoryBytes;
|
||||
final double? uiFps;
|
||||
@@ -84,6 +88,8 @@ class PerformanceStats {
|
||||
this.cacheUsed,
|
||||
this.cacheSpeed,
|
||||
this.cacheDuration,
|
||||
this.dvConversionActive = false,
|
||||
this.dvConversionMode = '',
|
||||
this.appMemoryBytes,
|
||||
this.uiFps,
|
||||
});
|
||||
@@ -121,6 +127,8 @@ class PerformanceStats {
|
||||
cacheUsed = null,
|
||||
cacheSpeed = null,
|
||||
cacheDuration = null,
|
||||
dvConversionActive = false,
|
||||
dvConversionMode = '',
|
||||
appMemoryBytes = null,
|
||||
uiFps = null;
|
||||
|
||||
|
||||
@@ -192,6 +192,9 @@ class PerformanceStatsService {
|
||||
frameDropCount: statsMap['videoDroppedFrames'] as int?,
|
||||
// Buffer metrics - convert ms to seconds for duration
|
||||
cacheDuration: ((statsMap['totalBufferedDurationMs'] as int?) ?? 0) / 1000.0,
|
||||
// DV conversion
|
||||
dvConversionActive: statsMap['dvConversionActive'] == true,
|
||||
dvConversionMode: statsMap['dvConversionMode'] as String? ?? '',
|
||||
// App metrics
|
||||
appMemoryBytes: appMemory,
|
||||
uiFps: _currentUiFps,
|
||||
|
||||
Reference in New Issue
Block a user