本文基于大牛直播 SDK(SmartMediaKit)的鸿蒙 NEXT 移植版本,结合真实的 ArkTS 工程代码,完整介绍播放过程中如何对音量、画面亮度、对比度、饱和度进行实时动态调节。
背景
在监控预览、直播连麦等实时流媒体场景中,用户往往需要在不中断播放的前提下随时调整音量或画质参数。鸿蒙 NEXT(纯血鸿蒙)上的 SmartPlayer SDK 通过 Native 接口将这些能力透出给 ArkTS 层,整个调用链路清晰:
swift
ArkTS UI(SmartPlayerPage)
↓ @State 绑定 / onValueChange 回调
SmartPlayerWrapper(ArkTS 封装层)
↓ NT 命名空间
SmartPlayerNative(libSmartPlayer.so NAPI 映射)
↓
C++ 原生层(OHAudio 音量 / GLES2 色彩矩阵)
所有可实时调节的参数,在 SmartPlayerWrapper 内部都有对应的 Session 状态缓存 ,页面重建或 Surface 重绑后会通过 reapplySavedConfig() 自动恢复,不需要调用方手动记录。
一、接口速览
Native 层(SmartPlayerNative.ets)
javascript
// 音量(0 ~ 100 整数)
export function SetSmartPlayerVolume(h: number, v: number): number
// 亮度:先 enable,再 set
export function SmartPlayerEnableVideoBrightnessOption(h: number, v: number): number
export function SmartPlayerSetVideoBrightness(h: number, v: number): number
// 对比度:先 enable,再 set
export function SmartPlayerEnableVideoContrastOption(h: number, v: number): number
export function SmartPlayerSetVideoContrast(h: number, v: number): number
// 饱和度:先 enable,再 set
export function SmartPlayerEnableVideoSaturationOption(h: number, v: number): number
export function SmartPlayerSetVideoSaturation(h: number, v: number): number
注意 :亮度/对比度/饱和度三个选项都有独立的
enable开关。首次调用前必须先enable(true),否则set不生效。
Wrapper 层(SmartPlayerWrapper.ets)
Wrapper 在 Native 调用外包了一层 isOpened() 保护,并在成功后同步更新 Session 缓存:
kotlin
// 音量
setVolume(volume: number): boolean {
if (!this.isOpened()) return false;
const ret = NT.SetSmartPlayerVolume(this.handle, volume) === SmartPlayerResult.OK;
const s = this.getSession();
if (ret && s) s.volume = volume;
return ret;
}
// 亮度(enable + set 拆分为两个方法)
enableVideoBrightnessOption(enable: boolean): boolean {
if (!this.isOpened()) return false;
return NT.SmartPlayerEnableVideoBrightnessOption(this.handle, enable ? 1 : 0) === SmartPlayerResult.OK;
}
setVideoBrightness(value: number): boolean {
if (!this.isOpened()) return false;
const ret = NT.SmartPlayerSetVideoBrightness(this.handle, value) === SmartPlayerResult.OK;
const s = this.getSession();
if (ret && s) s.imageBrightnessValue = value;
return ret;
}
// 对比度、饱和度同理(enableVideoContrastOption / setVideoContrast,enableVideoSaturationOption / setVideoSaturation)
二、UI 层设计:AdjustSliderRow 组件
编辑
ArkUI 的 Slider 存在一个已知问题:在父组件 @State 更新时,Slider 内部位置不会自动回弹(双向绑定失效)。为此,项目封装了 AdjustSliderRow 子组件,用 @Link 把状态直接传入,彻底绕过这个问题:
typescript
@Component
struct AdjustSliderRow {
title: string = '';
@Link value: number; // @Link 双向绑定,解决回弹问题
activeColor: string = '';
useNeutralColorRule: boolean = true;
onValueChange: (value: number) => void = () => {};
private clampAdjustValue(val: number): number {
return Math.min(100, Math.max(0, Math.round(val))); // 值域钳制 0~100
}
build() {
Row({ space: 8 }) {
Text(this.title).fontSize(13).fontColor('#666666').width(56)
Slider({ value: this.value, min: 0, max: 100, step: 1, style: SliderStyle.OutSet })
.layoutWeight(1)
.showTips(true)
.selectedColor(this.activeColor)
.trackColor('#D9D9D9')
.blockColor(this.activeColor)
.onChange((sliderValue: number) => {
const nextValue = this.clampAdjustValue(sliderValue);
this.value = nextValue; // 同步到父组件 @State
this.onValueChange(nextValue); // 触发 Native 调用
})
Text(`${this.value}`)
.fontSize(13)
// 中性值(50)显示灰色,其余显示主题色
.fontColor(this.useNeutralColorRule && this.value === 50 ? '#9E9E9E' : this.activeColor)
.width(40)
.textAlign(TextAlign.End)
}
.width('100%')
}
}
useNeutralColorRule 这个参数值得注意:对于亮度/对比度/饱和度,50 是默认中性值(无效果),用灰色渲染提示用户当前没有偏移;而音量没有"中性"概念,所以传 false。
三、实时调节区域完整实现
3.1 页面状态声明
less
@State volume: number = 100; // 音量,0~100
@State imageBrightnessValue: number = 50; // 亮度,默认中性 50
@State imageContrastValue: number = 50; // 对比度,默认中性 50
@State imageSaturationValue: number = 50; // 饱和度,默认中性 50
3.2 UI 构建
php
@Builder
private buildRealtimeAdjustSection() {
Column({ space: 10 }) {
Row() {
Text('音量 / 画面实时调节')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Blank()
Button('重置图像')
.height(30)
.fontSize(12)
.backgroundColor('#607D8B')
.fontColor(Color.White)
.borderRadius(15)
.padding({ left: 12, right: 12 })
.onClick(() => { this.resetImageAdjustments(); })
}
.width('100%')
// 音量滑条(无中性色规则,activeColor 蓝色)
AdjustSliderRow({
title: '音量',
value: $volume,
activeColor: '#2196F3',
useNeutralColorRule: false,
onValueChange: (val: number) => { this.applyAudioVolume(val); }
})
// 亮度滑条(有中性色规则,activeColor 橙色)
AdjustSliderRow({
title: '亮度',
value: $imageBrightnessValue,
activeColor: '#FF9800',
useNeutralColorRule: true,
onValueChange: (val: number) => { this.applyImageBrightness(val); }
})
// 对比度滑条(有中性色规则,activeColor 靛蓝)
AdjustSliderRow({
title: '对比度',
value: $imageContrastValue,
activeColor: '#3F51B5',
useNeutralColorRule: true,
onValueChange: (val: number) => { this.applyImageContrast(val); }
})
// 饱和度滑条(有中性色规则,activeColor 绿色)
AdjustSliderRow({
title: '饱和度',
value: $imageSaturationValue,
activeColor: '#4CAF50',
useNeutralColorRule: true,
onValueChange: (val: number) => { this.applyImageSaturation(val); }
})
Text('范围 0~100,默认 50 为中性值;拖动时播放画面会实时生效。')
.fontSize(11)
.fontColor('#888888')
.width('100%')
}
.width('92%')
.padding(12)
.backgroundColor(Color.White)
.borderRadius(10)
}
3.3 实际调用方法
音量调节
音量最简单,直接一个调用,无需 enable 开关:
kotlin
private applyAudioVolume(value: number): void {
if (!this.player.isOpened()) return;
this.player.setVolume(value); // 0 静音,100 满音量,播放中立即生效
}
亮度调节
亮度需要先 enable,再 set。两步必须都成功才算完整:
typescript
private applyImageBrightness(value: number): void {
console.info(`${this.TAG} 触发亮度调节, 当前值: ${value}`);
if (!this.player.isOpened()) return;
this.player.enableVideoBrightnessOption(true); // 第一步:开启功能
const setRet = this.player.setVideoBrightness(value); // 第二步:写入值
console.info(`${this.TAG} setVideoBrightness ret: ${setRet}`);
}
对比度调节
typescript
private applyImageContrast(value: number): void {
console.info(`${this.TAG} 触发对比度调节, 当前值: ${value}`);
if (!this.player.isOpened()) return;
this.player.enableVideoContrastOption(true);
const setRet = this.player.setVideoContrast(value);
console.info(`${this.TAG} setVideoContrast ret: ${setRet}`);
}
饱和度调节
typescript
private applyImageSaturation(value: number): void {
console.info(`${this.TAG} 触发饱和度调节, 当前值: ${value}`);
if (!this.player.isOpened()) return;
this.player.enableVideoSaturationOption(true);
const setRet = this.player.setVideoSaturation(value);
console.info(`${this.TAG} setVideoSaturation ret: ${setRet}`);
}
一键重置图像
重置时将三个 @State 值归回 50,然后直接调 set(不需要再 enable,因为 enable 在 applyPlaybackConfig 时已经一次性开启过):
kotlin
private resetImageAdjustments(): void {
this.imageBrightnessValue = 50;
this.imageContrastValue = 50;
this.imageSaturationValue = 50;
if (!this.player.isOpened()) return;
this.player.setVideoBrightness(this.imageBrightnessValue);
this.player.setVideoContrast(this.imageContrastValue);
this.player.setVideoSaturation(this.imageSaturationValue);
}
四、播放启动时的初始化:applyPlaybackConfig
调节功能在播放启动阶段 也要完成一次完整的初始化,写入 config 里保存的默认值,并且把三个 enable 一并打开。这个逻辑集中在 SmartPlayerWrapper.applyPlaybackConfig() 中:
ini
applyPlaybackConfig(config: PlayerCommonConfig): boolean {
if (!this.isOpened()) return false;
let ok = this.applyBaseConfig(config);
// ...其他配置省略...
// 硬解码 Surface 直通模式(mode=2)下无法做色彩处理,跳过
const enableImageOptions = config.videoDecoderMode !== 2;
if (enableImageOptions) {
ok = this.enableVideoBrightnessOption(true) && ok;
ok = this.enableVideoContrastOption(true) && ok;
ok = this.enableVideoSaturationOption(true) && ok;
ok = this.setVideoBrightness(config.imageBrightnessValue) && ok;
ok = this.setVideoContrast(config.imageContrastValue) && ok;
ok = this.setVideoSaturation(config.imageSaturationValue) && ok;
}
return ok;
}
这里有一个重要的约束:硬解码 + Surface 直通模式(videoDecoderMode = 2)不支持色彩调节。Surface 直通时视频帧直接写入 OHNativeWindow,绕过了软件色彩矩阵,亮度/对比度/饱和度的操作在这种模式下会被跳过。如果业务场景需要色彩调节,应选择软解码(mode=0)或普通硬解码(mode=1)。

HarmonyOS NEXT纯血鸿蒙RTSP|RTMP播放器
五、常见问题
Q:调节亮度后画面没有变化?
最常见的原因有两个:一是漏掉了 enableVideoBrightnessOption(true) 这一步,enable 和 set 是成对的,缺一不可;二是当前解码模式是 Surface 直通(mode=2),该模式下色彩调节不生效,需要切换到软解码或普通硬解码。
Q:滑条拖动后松手会弹回原位?
这是 ArkUI Slider 的已知行为:当父组件 @State 在同一帧更新时,Slider 的内部位置可能不跟随。解决方案就是文中的 AdjustSliderRow 子组件------将状态用 @Link 传入子组件,让 Slider 与状态源在同一组件树节点内,即可消除回弹。
六、完整调用时序
scss
SmartPlayerOpen() // 获取 handle
↓
applyPlaybackConfig() // 初始化,含 enable + set 色彩默认值
↓
SmartPlayerStartPlayback() // 开始播放
↓
── 播放过程中 ──────────────────────────────────
setVolume(val) // 随时调节音量(0~100)
enableVideoBrightnessOption(true) → setVideoBrightness(val) // 亮度
enableVideoContrastOption(true) → setVideoContrast(val) // 对比度
enableVideoSaturationOption(true) → setVideoSaturation(val) // 饱和度
↓
SmartPlayerStopPlayback() // 停止
SmartPlayerClose() // 释放
小结
大牛直播 SDK 在鸿蒙 NEXT 上把音量和色彩调节的接口设计得相当完整。音量是无状态的单一调用,色彩三项则需要 enable + set 两步配合,这个细节在初次集成时容易忽略。此外,调节效果依赖解码模式,Surface 直通这条路子虽然延迟最低,但牺牲了所有后处理能力,实际项目中需要根据业务需要权衡取舍。
整体来看,借助 SmartPlayerWrapper 的 Session 缓存机制,调节值在 Surface 重建后可以自动恢复,UI 层只需要维护好 @State 变量和滑条的双向绑定,其余都由 SDK 内部处理,接入成本并不高。