嵌入式音频开发(3)- AudioService核心功能

音量调节

TV/STB等产品的音量调节主要是通过遥控器上的音量键实现。具体到AudioService则是adjustSuggestedStreamVolume函数。

adjustSuggestedStreamVolume

java 复制代码
 /** All callers come from platform apps/system server, so no attribution tag is needed */
    private void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags,
            String callingPackage, String caller, int uid, int pid, boolean hasModifyAudioSettings,
            int keyEventMode) {

		//判断是否有外部的音量控制器。这个功能没用过,但猜测是可以bypass Android的音量处理,有厂商自己定制音量调节的实现。
        boolean hasExternalVolumeController = notifyExternalVolumeController(direction);
        if (hasExternalVolumeController) {
            return;
        }

        final int streamType;
        synchronized (mForceControlStreamLock) {
            // Request lock in case mVolumeControlStream is changed by other thread.
            if (mUserSelectedVolumeControlStream) { // implies mVolumeControlStream != -1
                streamType = mVolumeControlStream;
            } else {
                final int maybeActiveStreamType = getActiveStreamType(suggestedStreamType);
                final boolean activeForReal;
                if (maybeActiveStreamType == AudioSystem.STREAM_RING
                        || maybeActiveStreamType == AudioSystem.STREAM_NOTIFICATION) {
                    activeForReal = wasStreamActiveRecently(maybeActiveStreamType, 0);
                } else {
                    activeForReal = mAudioSystem.isStreamActive(maybeActiveStreamType, 0);
                }
                if (activeForReal || mVolumeControlStream == -1) {
                    streamType = maybeActiveStreamType;
                } else {
                    streamType = mVolumeControlStream;
                }
            }
        }

        final boolean isMute = isMuteAdjust(direction);

        ensureValidStreamType(streamType);
        final int resolvedStream = mStreamVolumeAlias[streamType];

        // Play sounds on STREAM_RING only.
        if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 &&
                resolvedStream != AudioSystem.STREAM_RING) {
            flags &= ~AudioManager.FLAG_PLAY_SOUND;
        }

        // For notifications/ring, show the ui before making any adjustments
        // Don't suppress mute/unmute requests
        // Don't suppress adjustments for single volume device
        if (mVolumeController.suppressAdjustment(resolvedStream, flags, isMute)
                && !mIsSingleVolume) {
            direction = 0;
            flags &= ~AudioManager.FLAG_PLAY_SOUND;
            flags &= ~AudioManager.FLAG_VIBRATE;
            if (DEBUG_VOL) Log.d(TAG, "Volume controller suppressed adjustment");
        }

        adjustStreamVolume(streamType, direction, flags, callingPackage, caller, uid, pid,
                null, hasModifyAudioSettings, keyEventMode);
    }

adjustSuggestedStreamVolume针对各种音量调节过程中涉及的场景做完相应的处理后(这部分代码有个大概的印象就可以了,如此琐碎很难记住),最后调用到adjustStreamVolume。

adjustStreamVolume

audioservice的代码是真的长,只讲解一些比较常见的逻辑。受限于开发经验,如果遗漏还请见谅。重点逻辑已使用中文做注释。

java 复制代码
    protected void adjustStreamVolume(int streamType, int direction, int flags,
            String callingPackage, String caller, int uid, int pid, String attributionTag,
            boolean hasModifyAudioSettings, int keyEventMode) {
        if (mUseFixedVolume) {
            return;
        }
        if (DEBUG_VOL) Log.d(TAG, "adjustStreamVolume() stream=" + streamType + ", dir=" + direction
                + ", flags=" + flags + ", caller=" + caller);

        ensureValidDirection(direction);
        ensureValidStreamType(streamType);

        boolean isMuteAdjust = isMuteAdjust(direction);

        if (isMuteAdjust && !isStreamAffectedByMute(streamType)) {
            return;
        }

        // If adjust is mute and the stream is STREAM_VOICE_CALL or STREAM_BLUETOOTH_SCO, make sure
        // that the calling app have the MODIFY_PHONE_STATE permission.
        if (isMuteAdjust &&
            (streamType == AudioSystem.STREAM_VOICE_CALL ||
                streamType == AudioSystem.STREAM_BLUETOOTH_SCO) &&
                mContext.checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, pid, uid)
                    != PackageManager.PERMISSION_GRANTED) {
            Log.w(TAG, "MODIFY_PHONE_STATE Permission Denial: adjustStreamVolume from pid="
                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
            return;
        }

        // If the stream is STREAM_ASSISTANT,
        // make sure that the calling app have the MODIFY_AUDIO_ROUTING permission.
        if (streamType == AudioSystem.STREAM_ASSISTANT &&
                mContext.checkPermission(
                android.Manifest.permission.MODIFY_AUDIO_ROUTING, pid, uid)
                    != PackageManager.PERMISSION_GRANTED) {
            Log.w(TAG, "MODIFY_AUDIO_ROUTING Permission Denial: adjustStreamVolume from pid="
                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
            return;
        }

        // use stream type alias here so that streams with same alias have the same behavior,
        // including with regard to silent mode control (e.g the use of STREAM_RING below and in
        // checkForRingerModeChange() in place of STREAM_RING or STREAM_NOTIFICATION)
        //获得streamtype对应的别名,所谓别名就是可以让一些streamtype共享音量一套音量调节
        int streamTypeAlias = mStreamVolumeAlias[streamType];

        VolumeStreamState streamState = mStreamStates[streamTypeAlias];
        //获得streamtype当前正在输出的设备类型
        final int device = getDeviceForStream(streamTypeAlias);

        int aliasIndex = streamState.getIndex(device);
        boolean adjustVolume = true;
        int step;
         
         //所谓蓝牙的绝对音量就是音量算法由蓝牙设备执行。而非绝对音量则是由audiofinger的audiomixer执行。
        // skip a2dp absolute volume control request when the device
        // is neither an a2dp device nor BLE device
        if ((!AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
                && !AudioSystem.DEVICE_OUT_ALL_BLE_SET.contains(device))
                && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) != 0) {
            return;
        }

        // If we are being called by the system (e.g. hardware keys) check for current user
        // so we handle user restrictions correctly.
        if (uid == android.os.Process.SYSTEM_UID) {
            uid = UserHandle.getUid(getCurrentUserId(), UserHandle.getAppId(uid));
        }
        // validate calling package and app op
        if (!checkNoteAppOp(
                STREAM_VOLUME_OPS[streamTypeAlias], uid, callingPackage, attributionTag)) {
            return;
        }

        // reset any pending volume command
        synchronized (mSafeMediaVolumeStateLock) {
            mPendingVolumeCommand = null;
        }

        flags &= ~AudioManager.FLAG_FIXED_VOLUME;
        if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
            flags |= AudioManager.FLAG_FIXED_VOLUME;

            // Always toggle between max safe volume and 0 for fixed volume devices where safe
            // volume is enforced, and max and 0 for the others.
            // This is simulated by stepping by the full allowed volume range
            if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE &&
                    mSafeMediaVolumeDevices.contains(device)) {
                step = safeMediaVolumeIndex(device);
            } else {
                step = streamState.getMaxIndex();
            }
            if (aliasIndex != 0) {
                aliasIndex = step;
            }
        } else {
             //计算音量调节步幅(按键每次步进是1)
            // convert one UI step (+/-1) into a number of internal units on the stream alias
            step = rescaleStep(10, streamType, streamTypeAlias);
        }

        // If either the client forces allowing ringer modes for this adjustment,
        // or the stream type is one that is affected by ringer modes
        if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
                (isUiSoundsStreamType(streamTypeAlias))) {
            int ringerMode = getRingerModeInternal();
            // do not vibrate if already in vibrate mode
            if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
                flags &= ~AudioManager.FLAG_VIBRATE;
            }
            // Check if the ringer mode handles this adjustment. If it does we don't
            // need to adjust the volume further.
            final int result = checkForRingerModeChange(aliasIndex, direction, step,
                    streamState.mIsMuted, callingPackage, flags);
            adjustVolume = (result & FLAG_ADJUST_VOLUME) != 0;
            // If suppressing a volume adjustment in silent mode, display the UI hint
            if ((result & AudioManager.FLAG_SHOW_SILENT_HINT) != 0) {
                flags |= AudioManager.FLAG_SHOW_SILENT_HINT;
            }
            // If suppressing a volume down adjustment in vibrate mode, display the UI hint
            if ((result & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0) {
                flags |= AudioManager.FLAG_SHOW_VIBRATE_HINT;
            }
        }

        // If the ringer mode or zen is muting the stream, do not change stream unless
        // it'll cause us to exit dnd
        if (!volumeAdjustmentAllowedByDnd(streamTypeAlias, flags)) {
            adjustVolume = false;
        }
        int oldIndex = mStreamStates[streamType].getIndex(device);

        // Check if the volume adjustment should be handled by an absolute volume controller instead
        if (isAbsoluteVolumeDevice(device)
                && (flags & AudioManager.FLAG_ABSOLUTE_VOLUME) == 0) {
            AbsoluteVolumeDeviceInfo info = mAbsoluteVolumeDeviceInfoMap.get(device);
            if (info.mHandlesVolumeAdjustment) {
                dispatchAbsoluteVolumeAdjusted(streamType, info, oldIndex, direction,
                        keyEventMode);
                return;
            }
        }

        if (adjustVolume && (direction != AudioManager.ADJUST_SAME)
                && (keyEventMode != AudioDeviceVolumeManager.ADJUST_MODE_END)) {
            mAudioHandler.removeMessages(MSG_UNMUTE_STREAM);

            if (isMuteAdjust && !mFullVolumeDevices.contains(device)) {
                boolean state;
                if (direction == AudioManager.ADJUST_TOGGLE_MUTE) {
                    state = !streamState.mIsMuted;
                } else {
                    state = direction == AudioManager.ADJUST_MUTE;
                }
                muteAliasStreams(streamTypeAlias, state);
            } else if ((direction == AudioManager.ADJUST_RAISE) &&
                    !checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
                Log.e(TAG, "adjustStreamVolume() safe volume index = " + oldIndex);
                mVolumeController.postDisplaySafeVolumeWarning(flags);
            } else if (!isFullVolumeDevice(device)
            //在此处修改stream对应的index
                    && (streamState.adjustIndex(direction * step, device, caller,
                            hasModifyAudioSettings)
                            || streamState.mIsMuted)) {
                // Post message to set system volume (it in turn will post a
                // message to persist).
                if (streamState.mIsMuted) {
                    // Unmute the stream if it was previously muted
                    if (direction == AudioManager.ADJUST_RAISE) {
                        // unmute immediately for volume up
                        muteAliasStreams(streamTypeAlias, false);
                    } else if (direction == AudioManager.ADJUST_LOWER) {
                        if (mIsSingleVolume) {
                            sendMsg(mAudioHandler, MSG_UNMUTE_STREAM, SENDMSG_QUEUE,
                                    streamTypeAlias, flags, null, UNMUTE_STREAM_DELAY);
                        }
                    }
                }
				//重点来了,发送设置设备音量的消息,这个调用会使得音量修改到达底层audioflinger并真正生效
                sendMsg(mAudioHandler,
                        MSG_SET_DEVICE_VOLUME,
                        SENDMSG_QUEUE,
                        device,
                        0,
                        streamState,
                        0);
            }

            int newIndex = mStreamStates[streamType].getIndex(device);

            // Check if volume update should be send to AVRCP
            if (streamTypeAlias == AudioSystem.STREAM_MUSIC
                    && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
                    && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
                if (DEBUG_VOL) {
                    Log.d(TAG, "adjustSreamVolume: postSetAvrcpAbsoluteVolumeIndex index="
                            + newIndex + "stream=" + streamType);
                }
                mDeviceBroker.postSetAvrcpAbsoluteVolumeIndex(newIndex / 10);
            } else if (isAbsoluteVolumeDevice(device)
                    && (flags & AudioManager.FLAG_ABSOLUTE_VOLUME) == 0) {
                AbsoluteVolumeDeviceInfo info = mAbsoluteVolumeDeviceInfoMap.get(device);
                dispatchAbsoluteVolumeChanged(streamType, info, newIndex);
            }

            if (AudioSystem.isLeAudioDeviceType(device)
                    && streamType == getBluetoothContextualVolumeStream()
                    && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
                if (DEBUG_VOL) {
                    Log.d(TAG, "adjustSreamVolume postSetLeAudioVolumeIndex index="
                            + newIndex + " stream=" + streamType);
                }
                mDeviceBroker.postSetLeAudioVolumeIndex(newIndex,
                    mStreamStates[streamType].getMaxIndex(), streamType);
            }

            // Check if volume update should be send to Hearing Aid
            if (device == AudioSystem.DEVICE_OUT_HEARING_AID) {
                // only modify the hearing aid attenuation when the stream to modify matches
                // the one expected by the hearing aid
                if (streamType == getBluetoothContextualVolumeStream()) {
                    if (DEBUG_VOL) {
                        Log.d(TAG, "adjustSreamVolume postSetHearingAidVolumeIndex index="
                                + newIndex + " stream=" + streamType);
                    }
                    mDeviceBroker.postSetHearingAidVolumeIndex(newIndex, streamType);
                }
            }
        }

        final int newIndex = mStreamStates[streamType].getIndex(device);

        if (adjustVolume) {
            //针对接有HDMI设备并且适配了HDMI相关接口的产品
            synchronized (mHdmiClientLock) {
                if (mHdmiManager != null) {
                    // At most one of mHdmiPlaybackClient and mHdmiTvClient should be non-null
                    HdmiClient fullVolumeHdmiClient = mHdmiPlaybackClient;
                    if (mHdmiTvClient != null) {
                        fullVolumeHdmiClient = mHdmiTvClient;
                    }

                    if (fullVolumeHdmiClient != null
                            && mHdmiCecVolumeControlEnabled
                            && streamTypeAlias == AudioSystem.STREAM_MUSIC
                            // vol change on a full volume device
                            && isFullVolumeDevice(device)) {
                        int keyCode = KeyEvent.KEYCODE_UNKNOWN;
                        switch (direction) {
                            case AudioManager.ADJUST_RAISE:
                                keyCode = KeyEvent.KEYCODE_VOLUME_UP;
                                break;
                            case AudioManager.ADJUST_LOWER:
                                keyCode = KeyEvent.KEYCODE_VOLUME_DOWN;
                                break;
                            case AudioManager.ADJUST_TOGGLE_MUTE:
                            case AudioManager.ADJUST_MUTE:
                            case AudioManager.ADJUST_UNMUTE:
                                // Many CEC devices only support toggle mute. Therefore, we send the
                                // same keycode for all three mute options.
                                keyCode = KeyEvent.KEYCODE_VOLUME_MUTE;
                                break;
                            default:
                                break;
                        }
                        if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
                            final long ident = Binder.clearCallingIdentity();
                            try {
                                switch (keyEventMode) {
                                    case AudioDeviceVolumeManager.ADJUST_MODE_NORMAL:
                                        fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, true);
                                        fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, false);
                                        break;
                                    case AudioDeviceVolumeManager.ADJUST_MODE_START:
                                        fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, true);
                                        break;
                                    case AudioDeviceVolumeManager.ADJUST_MODE_END:
                                        fullVolumeHdmiClient.sendVolumeKeyEvent(keyCode, false);
                                        break;
                                    default:
                                        Log.e(TAG, "Invalid keyEventMode " + keyEventMode);
                                }
                            } finally {
                                Binder.restoreCallingIdentity(ident);
                            }
                        }
                    }

                    if (streamTypeAlias == AudioSystem.STREAM_MUSIC
                            && (oldIndex != newIndex || isMuteAdjust)) {
                        maybeSendSystemAudioStatusCommand(isMuteAdjust);
                    }
                }
            }
        }
        //发送音量改变的通知
        sendVolumeUpdate(streamType, oldIndex, newIndex, flags, device);
    }

adjustStreamVolume里面的代码同样很长,涉及到很多不同的场景要处理。主脉络是

1.修改streamtype的index。我们在此处加log可以判断音量值(index)是否调节符合预期。

java 复制代码
streamState.adjustIndex(direction * step, device, caller,
                            hasModifyAudioSettings)

2.发送消息(message)将刷新后的音量值(index)设置到底层。就是这行代码。

java 复制代码
  sendMsg(mAudioHandler,
                        MSG_SET_DEVICE_VOLUME,
                        SENDMSG_QUEUE,
                        device,
                        0,
                        streamState,
                        0);

MSG_SET_DEVICE_VOLUME消息处理

Audioservice内部有个handler负责将client对server的调用全部顺序执行。

java 复制代码
 public void handleMessage(Message msg) {
5350              switch (msg.what) {
5351  
5352                  case MSG_SET_DEVICE_VOLUME:
5353                      setDeviceVolume((VolumeStreamState) msg.obj, msg.arg1);
5354                      break;
5355  
java 复制代码
   /*package*/ void setDeviceVolume(VolumeStreamState streamState, int device) {
5033  
5034          final boolean isAvrcpAbsVolSupported = mDeviceBroker.isAvrcpAbsoluteVolumeSupported();
5035  
5036          synchronized (VolumeStreamState.class) {
5037              // Apply volume
                  //将VolumeStreamState的index值设置到底层
5038              streamState.applyDeviceVolume_syncVSS(device, isAvrcpAbsVolSupported);
5039  
5040              // Apply change to all streams using this one as alias
                  //将使用当前streamtype作为别名的streamtype的index也同步设置到底层
5041              int numStreamTypes = AudioSystem.getNumStreamTypes();
5042              for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
5043                  if (streamType != streamState.mStreamType &&
5044                          mStreamVolumeAlias[streamType] == streamState.mStreamType) {
5045                      // Make sure volume is also maxed out on A2DP device for aliased stream
5046                      // that may have a different device selected
5047                      int streamDevice = getDeviceForStream(streamType);
5048                      if ((device != streamDevice) && isAvrcpAbsVolSupported
5049                              && ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0)) {
5050                          mStreamStates[streamType].applyDeviceVolume_syncVSS(device,
5051                                  isAvrcpAbsVolSupported);
5052                      }
5053                      mStreamStates[streamType].applyDeviceVolume_syncVSS(streamDevice,
5054                              isAvrcpAbsVolSupported);
5055                  }
5056              }
5057          }
5058          // Post a persist volume msg
              //将VolumeStreamState的index保存到settings进行持久化
5059          sendMsg(mAudioHandler,
5060                  MSG_PERSIST_VOLUME,
5061                  SENDMSG_QUEUE,
5062                  device,
5063                  0,
5064                  streamState,
5065                  PERSIST_DELAY);
5066  
5067      }

由setDeviceVolume函数负责处理MSG_SET_DEVICE_VOLUME消息。主要逻辑如下:

1.通过applyDeviceVolume_syncVSS函数将VolumeStreamState的index值同步设置到底层。VSS就是VolumeStreamState的缩写。

2.将使用当前streamtype作为别名的streamtype的index也同步设置到底层。

3.将VolumeStreamState的index保存到settings进行持久化

applyDeviceVolume_syncVSS

函数很简单,比较重要的一点是通过将index设置为0来实现mute。接着调用setStreamVolumeIndex函数。

java 复制代码
void applyDeviceVolume_syncVSS(int device, boolean isAvrcpAbsVolSupported) {
4656              int index;
4657              if (mIsMuted) {
4658                  index = 0;
4659              } else if ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 && isAvrcpAbsVolSupported) {
4660                  index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
4661              } else if ((device & mFullVolumeDevices) != 0) {
4662                  index = (mIndexMax + 5)/10;
4663              } else if ((device & AudioSystem.DEVICE_OUT_HEARING_AID) != 0) {
4664                  index = (mIndexMax + 5)/10;
4665              } else {
4666                  index = (getIndex(device) + 5)/10;
4667              }
4668              setStreamVolumeIndex(index, device);
4669          }

setStreamVolumeIndex

setStreamVolumeIndex函数最终将VolumeStreamState的mStreamType,index和device通过AudioSystem接口设置到底层。

java 复制代码
  private void setStreamVolumeIndex(int index, int device) {
4645              // Only set audio policy BT SCO stream volume to 0 when the stream is actually muted.
4646              // This allows RX path muting by the audio HAL only when explicitly muted but not when
4647              // index is just set to 0 to repect BT requirements
4648              if (mStreamType == AudioSystem.STREAM_BLUETOOTH_SCO && index == 0 && !mIsMuted) {
4649                  index = 1;
4650              }
4651              AudioSystem.setStreamVolumeIndexAS(mStreamType, index, device);
4652          }
4653  

MSG_PERSIST_VOLUME消息处理

通过调用persistVolume函数处理MSG_PERSIST_VOLUME消息。

java 复制代码
5360                  case MSG_PERSIST_VOLUME:
5361                      persistVolume((VolumeStreamState) msg.obj, msg.arg1);
5362                      break;

persistVolume持久化音量index

通过Android系统的ContentResolver机制对音量的index进行持久化。对ContentResolver机制感兴趣的同学可以搜集资料了解一下。根据AudioService持久化音量的方式,我们可以在自己的应用里面监听音量index的变化以实现某些定制需求。

java 复制代码
        private void persistVolume(VolumeStreamState streamState, int device) {
5088              if (mUseFixedVolume) {
5089                  return;
5090              }
5091              if (mIsSingleVolume && (streamState.mStreamType != AudioSystem.STREAM_MUSIC)) {
5092                  return;
5093              }
5094              if (streamState.hasValidSettingsName()) {
5095                  System.putIntForUser(mContentResolver,
5096                          streamState.getSettingNameForDevice(device),
5097                          (streamState.getIndex(device) + 5)/ 10,
5098                          UserHandle.USER_CURRENT);
5099              }
5100          }

至此,调节音量在AudioService中的实现已经分析完了。接下来的逻辑我们在后面继续讨论,现在做一下总结。

总结

1.AudioService通过VolumeStreamState维护每一种streamtype对应的index。

2.每个index由streamtype和device共同决定。

3.index设置到底层生效最终是通过AudioSystem的接口。(后续我们将会看到底层设置音量做了哪些工作)

4.Audioservice涉及和底层交互的逻辑都会通过消息机制(handler)进行异步序列化处理。

相关推荐
☺����5 小时前
实现自己的AI视频监控系统-第一章-视频拉流与解码2
开发语言·人工智能·python·音视频
☺����8 小时前
实现自己的AI视频监控系统-第一章-视频拉流与解码1
人工智能·python·音视频
Black_Rock_br9 小时前
本地部署的终极多面手:Qwen2.5-Omni-3B,视频剪、音频混、图像生、文本写全搞定
人工智能·音视频
顾道长生'16 小时前
(Arxiv-2025)SkyReels-A2:在视频扩散变换器中组合任意内容
人工智能·计算机视觉·音视频·多模态
9527华安16 小时前
FPGA实现Aurora 64B66B图像视频点对点传输,基于GTH高速收发器,提供2套工程源码和技术支持
fpga开发·音视频·aurora·gth·高速收发器·64b66b
小曾同学.com1 天前
【每天学点‘音视频’】前向纠错 和 漏包重传
音视频·fec·前向纠错
胖虎11 天前
(二十)深入了解 AVFoundation-编辑:使用 AVMutableVideoComposition 实现视频加水印与图层合成(下)——实战篇
音视频·视频编辑·视频添加水印
AI浩1 天前
跟踪不稳定目标:基于外观引导的运动建模实现无人机视频中的鲁棒多目标跟踪
目标跟踪·音视频·无人机
小学生波波1 天前
如何免费给视频加字幕
音视频·免费字幕·加字幕·剪映加字幕