第二个 Android 11 开发者预览版现已推出,快来测试并分享您的反馈吧

MediaPlayer 概览

Android 多媒体框架支持播放各种常见媒体类型,以便您轻松地将音频、视频和图片集成到应用中。您可以使用 MediaPlayer API,播放存储在应用资源(原始资源)内的媒体文件、文件系统中的独立文件或者通过网络连接获得的数据流中的音频或视频。

本文档向您介绍了如何编写与用户和系统互动的媒体播放应用,以实现良好的性能和用户体验。

注意:您只能通过标准输出设备播放音频数据。目前,标准输出设备是移动设备的扬声器或蓝牙耳机。您无法在通话期间播放会话音频中的声音文件。

基础知识

以下类用于在 Android 框架中播放声音和视频:

MediaPlayer
此类是用于播放声音和视频的主要 API。
AudioManager
此类用于管理设备上的音频源和音频输出。

清单声明

在开始使用 MediaPlayer 开发应用之前,请确保您的清单具有适当的声明,这样才能使用相关功能。

  • 互联网权限 - 如果您使用 MediaPlayer 流式播放基于网络的内容,则您的应用必须申请网络访问权限。
        <uses-permission android:name="android.permission.INTERNET" />
        
  • 唤醒锁定权限 - 如果播放器应用需要防止屏幕变暗或处理器进入休眠状态,或者要使用 MediaPlayer.setScreenOnWhilePlaying()MediaPlayer.setWakeMode() 方法,则您必须申请此权限。
        <uses-permission android:name="android.permission.WAKE_LOCK" />
        

使用 MediaPlayer

MediaPlayer 类是媒体框架最重要的组成部分之一。此类的对象能够获取、解码以及播放音频和视频,而且只需极少量设置。它支持多种不同的媒体源,例如:

  • 本地资源
  • 内部 URI,例如您可能从内容解析器那获取的 URI
  • 外部网址(流式传输)

有关 Android 支持的媒体格式列表,请参阅支持的媒体格式页面。

以下示例展示了如何播放作为本地原始资源(保存在应用的 res/raw/ 目录中)提供的音频:

Kotlin

    var mediaPlayer: MediaPlayer? = MediaPlayer.create(context, R.raw.sound_file_1)
    mediaPlayer?.start() // no need to call prepare(); create() does that for you
    

Java

    MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
    mediaPlayer.start(); // no need to call prepare(); create() does that for you
    

在本例中,“原始”资源是指系统不会尝试以任何特定方式解析的文件。不过,该资源的内容不应为原始音频。它应该是采用某种支持的格式且经过适当编码和格式化的媒体文件。

播放系统中本地可用的 URI(例如,您可以通过内容解析器获取)中的内容方法如下:

Kotlin

    val myUri: Uri = .... // initialize Uri here
    val mediaPlayer: MediaPlayer? = MediaPlayer().apply {
        setAudioStreamType(AudioManager.STREAM_MUSIC)
        setDataSource(applicationContext, myUri)
        prepare()
        start()
    }
    

Java

    Uri myUri = ....; // initialize Uri here
    MediaPlayer mediaPlayer = new MediaPlayer();
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    mediaPlayer.setDataSource(getApplicationContext(), myUri);
    mediaPlayer.prepare();
    mediaPlayer.start();
    

通过 HTTP 流式传输并播放远程网址上的内容如下所示:

Kotlin

    val url = "http://........" // your URL here
    val mediaPlayer: MediaPlayer? = MediaPlayer().apply {
        setAudioStreamType(AudioManager.STREAM_MUSIC)
        setDataSource(url)
        prepare() // might take long! (for buffering, etc)
        start()
    }
    

Java

    String url = "http://........"; // your URL here
    MediaPlayer mediaPlayer = new MediaPlayer();
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    mediaPlayer.setDataSource(url);
    mediaPlayer.prepare(); // might take long! (for buffering, etc)
    mediaPlayer.start();
    

注意:如果您传递某个网址以流式传输在线媒体文件,则该文件必须能够进行渐进式下载。

注意:使用 setDataSource() 时,您必须捕获或传递 IllegalArgumentExceptionIOException,因为您引用的文件可能并不存在。

异步准备

原则上,使用 MediaPlayer 会非常简单。不过,请务必注意,要正确地将其与典型的 Android 应用集成,还需执行一些额外操作。例如,对 prepare() 的调用可能需要很长时间来执行,因为它可能涉及获取和解码媒体数据。因此,与任何可能需要很长时间来执行的方法一样,切勿从应用的界面线程中调用它。这样做会导致界面挂起,直到系统返回该方法,这是一种非常糟糕的用户体验,并且可能会导致 ANR(应用无响应)错误。即使您预计资源会快速加载,但也请记得,界面中任何响应时间超过十分之一秒的操作都会导致明显的暂停,并让用户觉得您的应用运行缓慢。

为避免界面线程挂起,请生成其他线程来准备 MediaPlayer,并在准备工作完成后通知主线程。不过,尽管您可以自行编写线程逻辑,但此模式在使用 MediaPlayer 时非常普遍,因此框架通过 prepareAsync() 方法提供了一种完成此任务的便捷方式。此方法会在后台开始准备媒体,并立即返回。当媒体准备就绪后,系统会调用通过 setOnPreparedListener() 配置的 MediaPlayer.OnPreparedListeneronPrepared() 方法。

管理状态

您还应该记住,MediaPlayer 以状态为基础。也就是说,MediaPlayer 具有内部状态,您在编写代码时必须始终注意,某些操作仅在播放器处于特定状态时才有效。如果您在错误的状态下执行某项操作,则系统可能会抛出异常或导致其他不良行为。

MediaPlayer 类的参考文档显示了一个完整的状态图,该图说明了哪些方法可将 从一种状态变为另一种状态。例如,当您创建新的 MediaPlayer 时,它处于“Idle”状态。此时,您应该通过调用 setDataSource() 初始化该类,使其处于“Initialized”状态。然后,您必须使用 prepare()prepareAsync() 方法完成准备工作。当 MediaPlayer 准备就绪后,它便会进入“Prepared”状态,这也意味着您可以通过调用 start() 使其播放媒体内容。此时,如图所示,您可以通过调用 start()pause()seekTo() 等方法在“Started”、“Paused”和“PlaybackCompleted”状态之间切换。不过请注意,当您调用 stop() 时,除非您再次准备 MediaPlayer,否则将无法再次调用 start()

在编写与 MediaPlayer 对象互动的代码时,请始终牢记该状态图,因为从错误的状态调用其方法是导致错误的常见原因。

释放 MediaPlayer

MediaPlayer 会占用宝贵的系统资源。因此,您应该始终采取额外的预防措施,确保 MediaPlayer 实例保留的时间不会过长。完成该操作后,您应始终调用 release() 以确保分配给它的所有系统资源均已正确释放。例如,如果您使用 MediaPlayer,并且您的 Activity 接收到对 onStop() 的调用,则您必须释放该 MediaPlayer,因为当 Activity 未与用户互动时,保留该 MediaPlayer 并没有什么意义(除非您在后台播放媒体内容,这将在下一部分中介绍)。当然,当 Activity 恢复或重启时,您需要先创建一个新的 MediaPlayer 并再次完成准备工作,然后才能恢复播放。

以下代码段介绍了如何释放并取消 MediaPlayer

Kotlin

    mediaPlayer?.release()
    mediaPlayer = null
    

Java

    mediaPlayer.release();
    mediaPlayer = null;
    

例如,思考一下:如果您忘记在 Activity 停止时释放 MediaPlayer,但是在 Activity 重新启动时新建一个 MediaPlayer,则可能会出现哪些问题。如您所知,当用户更改屏幕方向(或以其他方式更改设备配置)时,系统会重启 Activity(默认情况下),因此当用户在纵向和横向之间来回旋转设备时,您可能会很快消耗掉所有的系统资源,因为每当方向更改时,您都会创建一个永远不会释放的新 MediaPlayer。(如需详细了解运行时重启,请参阅处理运行时更改。)

您可能想知道:当用户离开您的 Activity 后仍继续播放“后台媒体”(这与内置音乐应用的行为十分相似)会发生什么。在这种情况下,您需要的是由 Service 控制的 MediaPlayer,如下一部分中所述

在 Service 中使用 MediaPlayer

如果您希望即使当应用未在屏幕上显示时,应用仍会在后台播放媒体内容(也就是说,您希望应用在用户与其他应用互动时继续播放媒体内容),则您必须启动一个 Service 并由此控制 MediaPlayer 实例。您需要将 MediaPlayer 嵌入到 MediaBrowserServiceCompat Service 中,并使其在其他 Activity 中与 MediaBrowserCompat 进行互动。

您应该注意这种客户端/服务器设置。我们对在后台服务中运行的播放器如何与系统的其他部分进行互动设定了预期。如果您的应用未满足这些预期,则可能会导致用户体验不佳。要了解完整详情,请参阅构建音频应用

本部分介绍了在 Service 内部实现 MediaPlayer 时如何对其进行管理的特殊说明。

异步运行

首先,与 Activity 类似,Service 中的所有工作均默认在单个线程中完成 - 实际上,如果您从同一应用中运行 Activity 和 Service,则它们会默认使用相同的线程(“主线程”)。因此,Service 需要快速处理传入的 Intent,并且在响应它们时避免执行冗长的计算。如果需要完成大量的工作或可能会阻塞调用,则您必须异步执行这些任务:从您自己实现的其他线程异步执行,或使用框架的诸多工具进行异步处理。

例如,从您的主线程中使用 MediaPlayer 时,您应该调用 prepareAsync() 而非 prepare(),并实现 MediaPlayer.OnPreparedListener,以便在准备工作完成后获得通知并开始播放。例如:

Kotlin

    private const val ACTION_PLAY: String = "com.example.action.PLAY"

    class MyService: Service(), MediaPlayer.OnPreparedListener {

        private var mMediaPlayer: MediaPlayer? = null

        override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
            ...
            val action: String = intent.action
            when(action) {
                ACTION_PLAY -> {
                    mMediaPlayer = ... // initialize it here
                    mMediaPlayer?.apply {
                        setOnPreparedListener(this@MyService)
                        prepareAsync() // prepare async to not block main thread
                    }

                }
            }
            ...
        }

        /** Called when MediaPlayer is ready */
        override fun onPrepared(mediaPlayer: MediaPlayer) {
            mediaPlayer.start()
        }
    }
    

Java

    public class MyService extends Service implements MediaPlayer.OnPreparedListener {
        private static final String ACTION_PLAY = "com.example.action.PLAY";
        MediaPlayer mediaPlayer = null;

        public int onStartCommand(Intent intent, int flags, int startId) {
            ...
            if (intent.getAction().equals(ACTION_PLAY)) {
                mediaPlayer = ... // initialize it here
                mediaPlayer.setOnPreparedListener(this);
                mediaPlayer.prepareAsync(); // prepare async to not block main thread
            }
        }

        /** Called when MediaPlayer is ready */
        public void onPrepared(MediaPlayer player) {
            player.start();
        }
    }
    

处理异步错误

在同步操作中,系统通常会通过异常或错误代码来指示错误,但当您使用异步资源时,应确保以适当的方式向应用发出错误通知。对于 MediaPlayer,您可以实现 MediaPlayer.OnErrorListener 并在 MediaPlayer 实例中对其进行设置来发出错误通知:

Kotlin

    class MyService : Service(), MediaPlayer.OnErrorListener {

        private var mediaPlayer: MediaPlayer? = null

        fun initMediaPlayer() {
            // ...initialize the MediaPlayer here...
            mediaPlayer?.setOnErrorListener(this)
        }

        override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
            // ... react appropriately ...
            // The MediaPlayer has moved to the Error state, must be reset!
        }
    }
    

Java

    public class MyService extends Service implements MediaPlayer.OnErrorListener {
        MediaPlayer mediaPlayer;

        public void initMediaPlayer() {
            // ...initialize the MediaPlayer here...
            mediaPlayer.setOnErrorListener(this);
        }

        @Override
        public boolean onError(MediaPlayer mp, int what, int extra) {
            // ... react appropriately ...
            // The MediaPlayer has moved to the Error state, must be reset!
        }
    }
    

请务必注意,出现错误时,MediaPlayer 会进入“Error”状态(如需完整的状态图,请参阅 类的参考文档),而您必须先进行重置,然后才能再次使用它。

使用唤醒锁定

当设计在后台播放媒体内容的应用时,设备可能会在您的 Service 运行时进入休眠状态。由于 Android 系统尝试在设备处于休眠状态时节省电量,因此系统会尝试关闭手机上任何不必要的功能,包括 CPU 和 WLAN 硬件。不过,如果您的 Service 正在播放或流式传输音乐,则您需要防止系统干扰播放。

为了确保您的 Service 在这些情况下能继续运行,您必须使用“唤醒锁定”。唤醒锁定可以告诉系统:您的应用正在使用一些即使在手机处于闲置状态时也应该可用的功能。

注意:您应始终谨慎使用唤醒锁定,并只使其保留必要的时长,因为它们会显著缩短设备的电池续航时间。

为确保 CPU 在 MediaPlayer 播放时继续运行,请在初始化 时调用 setWakeMode() 方法。完成该操作后,MediaPlayer 会在播放时保持指定的锁定状态,并在暂停或停止播放时释放锁定:

Kotlin

    mediaPlayer = MediaPlayer().apply {
        // ... other initialization here ...
        setWakeMode(applicationContext, PowerManager.PARTIAL_WAKE_LOCK)
    }
    

Java

    mediaPlayer = new MediaPlayer();
    // ... other initialization here ...
    mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
    

不过,此示例中获取的唤醒锁定只能保证 CPU 保持唤醒状态。如果您使用 WLAN 并通过网络流式传输媒体内容,则您可能也希望保持 WifiLock,该锁定必须手动获取和释放。因此,当您开始使用远程网址准备 MediaPlayer 时,您应创建并获取 WLAN 锁定。例如:

Kotlin

    val wifiManager = getSystemService(Context.WIFI_SERVICE) as WifiManager
    val wifiLock: WifiManager.WifiLock =
        wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock")

    wifiLock.acquire()
    

Java

    WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
        .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");

    wifiLock.acquire();
    

当您暂停或停止媒体内容,或者当您不再需要网络时,应释放该锁定:

Kotlin

    wifiLock.release()
    

Java

    wifiLock.release();
    

进行清理

如前文所述,MediaPlayer 对象会消耗大量的系统资源,因此您应仅使其保留必要的时长,并在操作完成后调用 release()。请务必明确调用此清理方法,而非依赖于系统垃圾回收,因为垃圾回收器要经过一段时间才会回收 MediaPlayer,原因在于它仅对内存需求敏感,而对缺少其他媒体相关资源并不敏感。因此,当您使用 Service 时,应始终替换 onDestroy() 方法以确保释放 MediaPlayer

Kotlin

    class MyService : Service() {

        private var mediaPlayer: MediaPlayer? = null
        // ...

        override fun onDestroy() {
            super.onDestroy()
            mediaPlayer?.release()
        }
    }
    

Java

    public class MyService extends Service {
       MediaPlayer mediaPlayer;
       // ...

       @Override
       public void onDestroy() {
           super.onDestroy();
           if (mediaPlayer != null) mediaPlayer.release();
       }
    }
    

除了在关闭时释放您的 MediaPlayer 之外,您还应始终寻找其他释放机会。例如,如果您预计在很长一段时间都无法播放媒体内容(例如,在失去音频焦点后),则您应果断释放现有的 MediaPlayer 并在之后重新创建。另一方面,如果您预计只是短时间停止播放,则您可能应该保留 MediaPlayer,以避免再次创建和准备它所产生的开销。

数字版权管理 (DRM)

从 Android 8.0(API 级别 26)开始,MediaPlayer 包含支持播放受 DRM 保护的资料的 API。这些 API 与由 MediaDrm 提供的低级别 API 类似,但前者是在较高级别运行,并且不会公开底层提取器、DRM 和加密对象。

尽管 MediaPlayer DRM API 并不提供 MediaDrm 的完整功能,但它支持最常见的使用情形。当前实现可以处理以下内容类型:

  • 受 Widevine 保护的本地媒体文件
  • 受 Widevine 保护的远程/流式传输媒体文件

以下代码段演示了如何在简单的同步实现中使用新的 DRM MediaPlayer 方法。

要管理受 DRM 控制的媒体,您需要在 MediaPlayer 调用的常规流程中包含新的方法,如下所示:

Kotlin

    mediaPlayer?.apply {
        setDataSource()
        setOnDrmConfigHelper() // optional, for custom configuration
        prepare()
        drmInfo?.also {
            prepareDrm()
            getKeyRequest()
            provideKeyResponse()
        }

        // MediaPlayer is now ready to use
        start()
        // ...play/pause/resume...
        stop()
        releaseDrm()
    }
    

Java

    setDataSource();
    setOnDrmConfigHelper(); // optional, for custom configuration
    prepare();
    if (getDrmInfo() != null) {
      prepareDrm();
      getKeyRequest();
      provideKeyResponse();
    }

    // MediaPlayer is now ready to use
    start();
    // ...play/pause/resume...
    stop();
    releaseDrm();
    

首先,像往常一样初始化 MediaPlayer 对象并使用 setDataSource() 来设置其来源。然后,要使用 DRM,请执行以下步骤:

  1. 如果您希望应用执行自定义配置,请定义 OnDrmConfigHelper 接口,并使用 setOnDrmConfigHelper() 将其附加到播放器。
  2. 调用 prepare()
  3. 调用 getDrmInfo()。如果来源具有 DRM 内容,则该方法会返回一个非 null MediaPlayer.DrmInfo 值。

如果存在 MediaPlayer.DrmInfo

  1. 检查可用 UUID 的映射,然后选择一个。
  2. 通过调用 prepareDrm() 为当前来源准备 DRM 配置。
    • 如果您创建并注册了 OnDrmConfigHelper 回调,则系统会在执行 prepareDrm() 时调用该回调。这样一来,您就能够在打开 DRM 会话之前执行 DRM 属性的自定义配置。该回调会在名为 prepareDrm() 的线程中同步调用。要访问 DRM 属性,请调用 getDrmPropertyString()setDrmPropertyString()。避免执行冗长的操作。
    • 如果尚未配置设备,prepareDrm() 还会访问配置服务器来配置该设备。所需的时间因网络连接而有所不同。
  3. 要获取不透明的密钥请求字节数组以发送到许可服务器,请调用 getKeyRequest()
  4. 要向 DRM 引擎告知从许可服务器接收到的密钥响应,请调用 provideKeyResponse()。结果取决于密钥请求的类型:
    • 如果响应针对的是离线密钥请求,则结果为密钥组标识符。您可以将此密钥组标识符与 restoreKeys() 结合使用,以将这些密钥恢复到新的会话中。
    • 如果响应针对的是流式传输或释放请求,则结果为 null。

异步运行 prepareDrm()

默认情况下,prepareDrm() 会同步运行,阻塞会持续到准备工作完成。不过,在新设备上进行的首次 DRM 准备可能还需要进行配置,该问题由 prepareDrm() 在内部进行处理,并且由于涉及网络操作,可能需要一些时间才能完成。您可以通过定义和设置 MediaPlayer.OnDrmPreparedListener 来避免阻塞 prepareDrm()

当您设置 OnDrmPreparedListener 时,prepareDrm() 会在后台执行配置(如果需要)和准备工作。当配置和准备工作完成后,系统会调用监听器。您不应对调用顺序或运行监听器的线程做任何假设(除非监听器已注册处理程序线程)。系统可以在 prepareDrm() 返回前后调用该监听器。

异步设置 DRM

您可以创建和注册用于进行 DRM 准备的 MediaPlayer.OnDrmInfoListener 以及用于启动播放器的 MediaPlayer.OnDrmPreparedListener,从而异步初始化 DRM。它们能够与 prepareAsync() 结合使用,如下所示:

Kotlin

    setOnPreparedListener()
    setOnDrmInfoListener()
    setDataSource()
    prepareAsync()
    // ...

    // If the data source content is protected you receive a call to the onDrmInfo() callback.
    override fun onDrmInfo(mediaPlayer: MediaPlayer, drmInfo: MediaPlayer.DrmInfo) {
        mediaPlayer.apply {
            prepareDrm()
            getKeyRequest()
            provideKeyResponse()
        }
    }

    // When prepareAsync() finishes, you receive a call to the onPrepared() callback.
    // If there is a DRM, onDrmInfo() sets it up before executing this callback,
    // so you can start the player.
    override fun onPrepared(mediaPlayer: MediaPlayer) {
        mediaPlayer.start()
    }
    

Java

    setOnPreparedListener();
    setOnDrmInfoListener();
    setDataSource();
    prepareAsync();
    // ...

    // If the data source content is protected you receive a call to the onDrmInfo() callback.
    onDrmInfo() {
      prepareDrm();
      getKeyRequest();
      provideKeyResponse();
    }

    // When prepareAsync() finishes, you receive a call to the onPrepared() callback.
    // If there is a DRM, onDrmInfo() sets it up before executing this callback,
    // so you can start the player.
    onPrepared() {

    start();
    }
    

处理加密媒体

从 Android 8.0(API 级别 26)开始,MediaPlayer 还可以为基本的流类型 H.264 和 AAC 解密通用加密方案 (CENC) 和 HLS 样本级加密媒体 (METHOD=SAMPLE-AES)。之前支持全分段加密媒体 (METHOD=AES-128)。

从 ContentResolver 检索媒体

媒体播放器应用中可能有用的另一项功能是能够检索用户保存在设备上音乐。您可以通过查询 ContentResolver 找到外部媒体来完成该操作:

Kotlin

    val resolver: ContentResolver = contentResolver
    val uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    val cursor: Cursor? = resolver.query(uri, null, null, null, null)
    when {
        cursor == null -> {
            // query failed, handle error.
        }
        !cursor.moveToFirst() -> {
            // no media on the device
        }
        else -> {
            val titleColumn: Int = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE)
            val idColumn: Int = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media._ID)
            do {
                val thisId = cursor.getLong(idColumn)
                val thisTitle = cursor.getString(titleColumn)
                // ...process entry...
            } while (cursor.moveToNext())
        }
    }
    cursor?.close()
    

Java

    ContentResolver contentResolver = getContentResolver();
    Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
    Cursor cursor = contentResolver.query(uri, null, null, null, null);
    if (cursor == null) {
        // query failed, handle error.
    } else if (!cursor.moveToFirst()) {
        // no media on the device
    } else {
        int titleColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE);
        int idColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media._ID);
        do {
           long thisId = cursor.getLong(idColumn);
           String thisTitle = cursor.getString(titleColumn);
           // ...process entry...
        } while (cursor.moveToNext());
    }
    

要将其与 MediaPlayer 结合使用,您可以执行以下操作:

Kotlin

    val id: Long = /* retrieve it from somewhere */
    val contentUri: Uri =
        ContentUris.withAppendedId(android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id )

    mediaPlayer = MediaPlayer().apply {
        setAudioStreamType(AudioManager.STREAM_MUSIC)
        setDataSource(applicationContext, contentUri)
    }

    // ...prepare and start...
    

Java

    long id = /* retrieve it from somewhere */;
    Uri contentUri = ContentUris.withAppendedId(
            android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);

    mediaPlayer = new MediaPlayer();
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    mediaPlayer.setDataSource(getApplicationContext(), contentUri);

    // ...prepare and start...
    

示例代码

BasicMediaDecoderDeviceOwner 示例演示了如何使用本页介绍的 API。

了解详情

这些页面涵盖与录制、存储以及播放音频和视频相关的主题。