安卓进阶——多媒体

✅作者简介:大家好,我是 Meteors., 向往着更加简洁高效的代码写法与编程方式,持续分享Java技术内容。

🍎个人主页:Meteors.的博客

💞当前专栏:知识分享

✨特色专栏:知识分享

🥭本文内容:安卓进阶------多媒体

📚 ** ps ** :阅读文章如果有问题或者疑惑,欢迎在评论区提问或指出。


目录

[一. MediaPlayer](#一. MediaPlayer)

(一)介绍

[(二)基本使用步骤(以播放 Raw 资源为例)](#(二)基本使用步骤(以播放 Raw 资源为例))

[1. 创建 MediaPlayer 实例​​](#1. 创建 MediaPlayer 实例)

[​​2. 设置数据源​​](#2. 设置数据源)

[3. 准备播放器​​](#3. 准备播放器)

​​(1)同步准备​​:prepare()

​​​​(2)异步准备​​:prepareAsync()

[4. 开始播放​​](#4. 开始播放)

[5. ​​管理播放状态​​](#5. 管理播放状态)

[6. 释放资源(非常重要!)​​](#6. 释放资源(非常重要!))

[(三)MediaPlayer 的生命周期](#(三)MediaPlayer 的生命周期)

(三)完整代码示例

(四)最佳实践和注意事项

[二. SoundPool](#二. SoundPool)

(一)介绍

(二)特点

(三)基本使用步骤

[1. 创建 SoundPool 对象](#1. 创建 SoundPool 对象)

[2. 加载音频资源](#2. 加载音频资源)

[3. 设置加载完成监听](#3. 设置加载完成监听)

[4. 播放音频](#4. 播放音频)

[5. 控制播放](#5. 控制播放)

[6. 资源释放](#6. 资源释放)

[(四) 注意事项](#(四) 注意事项)

[三. VideoView](#三. VideoView)

(一)介绍

(二)特点

(三)基本使用步骤

[1. 布局文件中添加 VideoView](#1. 布局文件中添加 VideoView)

[2. 基本配置和播放控制](#2. 基本配置和播放控制)

(四)高级功能实现

[1. 自定义视频播放器](#1. 自定义视频播放器)

[2. 完整的视频播放器 Activity](#2. 完整的视频播放器 Activity)

[3. 对应的布局文件](#3. 对应的布局文件)

(五)网络视频播放配置

[1. 添加网络权限](#1. 添加网络权限)

[2. 处理网络视频缓冲](#2. 处理网络视频缓冲)

(六)注意事项

[1. 权限管理](#1. 权限管理)

[2. 格式支持:](#2. 格式支持:)

[3. 内存管理](#3. 内存管理)

[4. 网络视频](#4. 网络视频)

[5. 生命周期](#5. 生命周期)

[四. MediaRecorder](#四. MediaRecorder)

(一)介绍

(二)特点

(三)状态机

(四)基本使用步骤

[1. 权限配置](#1. 权限配置)

[2. 音频录制基础实现](#2. 音频录制基础实现)

[3. 视频录制基础实现](#3. 视频录制基础实现)

(五)高级功能实现

[1. 完整的录制管理器](#1. 完整的录制管理器)

[2. 完整的录制 Activity 实现](#2. 完整的录制 Activity 实现)

(六)对应布局文件

(七)录制按钮样式

(八)注意事项

[五. AudioRecord](#五. AudioRecord)

(一)介绍

(二)特点

(三)核心参数

[1. 音频参数说明](#1. 音频参数说明)

[2. 缓冲区计算](#2. 缓冲区计算)

(四)基本使用步骤

[1. 权限配置](#1. 权限配置)

[2. 基础 AudioRecord 实现](#2. 基础 AudioRecord 实现)

(五)高级功能实现

[1. 完整的音频录制管理器](#1. 完整的音频录制管理器)

[2. 实时音频分析示例](#2. 实时音频分析示例)

[3. 完整的录音 Activity](#3. 完整的录音 Activity)

(六)对应布局文件

(七)注意事项

(八)常见应用场景

[六. AudioTrack](#六. AudioTrack)

(一)介绍

(二)核心特点

(三)使用

[1. 基本配置](#1. 基本配置)

[2. 播放音频数据](#2. 播放音频数据)

[3. 流模式实时播放](#3. 流模式实时播放)

[4. 静态模式播放(适合短音效)](#4. 静态模式播放(适合短音效))

[5. 音量控制](#5. 音量控制)

(四)使用示例

(五)关键参数说明

[七. MediaRouter](#七. MediaRouter)

(一)介绍

(二)核心功能

(三)使用

[1. 添加依赖](#1. 添加依赖)

[2. 基本配置](#2. 基本配置)

[3. 设备选择和播放控制](#3. 设备选择和播放控制)

[4. 设备切换对话框](#4. 设备切换对话框)

[5. Chromecast 集成](#5. Chromecast 集成)

[6. 音频焦点管理](#6. 音频焦点管理)

(四)完整使用示例

(五)关键类说明

(六)常用路由类型

(七)注意事项


一. MediaPlayer

(一)介绍

MediaPlayer是 Android 系统提供的一个用于播放音频和视频文件的核心类。它功能强大,可以处理多种媒体源,包括:

  • ​本地资源​ ​:如 res/raw目录下的文件。

  • ​本地文件系统​​:如设备内部存储或 SD 卡中的文件。

  • ​网络流媒体​​:通过 HTTP/HTTPS 协议在线播放音频/视频。

  • ​应用资源​​:通过 URI 指定的内容。

​支持的格式​​:常见格式如 MP3, MP4, 3GP, OGG, WAV, AAC 等。但具体支持情况因设备硬件和系统版本而异。


(二)基本使用步骤(以播放 Raw 资源为例)

播放一个媒体文件最直接的流程如下:

1. 创建 MediaPlayer 实例​

Kotlin 复制代码
// 或者使用 create 快捷方法(对于资源文件) 
MediaPlayer mediaPlayer = new MediaPlayer(); 
// MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.my_sound);

​2. 设置数据源​

这是最关键的一步,告诉 MediaPlayer 要播放什么。

Kotlin 复制代码
// 示例1:播放 res/raw 下的音频
AssetFileDescriptor afd = context.getResources().openRawResourceFd(R.raw.my_sound);
mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
afd.close();

// 示例2:播放网络音频(需要声明网络权限)
mediaPlayer.setDataSource("https://example.com/audio.mp3");

// 示例3:播放本地文件
mediaPlayer.setDataSource("/sdcard/Music/song.mp3");

​注意​ ​:使用网络源必须要在 AndroidManifest.xml中添加 <uses-permission android:name="android.permission.INTERNET"/>权限。

3. 准备播放器​

在设置好数据源后,需要让 MediaPlayer 进入准备状态。有两种方式:

​(1)同步准备​ ​:prepare()

这是一个阻塞调用,会一直等待直到媒体准备完毕。​​不能在主线程(UI线程)中调用​ ​,否则会引发 NetworkOnMainThreadException(对于网络源)或导致界面卡顿。

Kotlin 复制代码
mediaPlayer.prepare(); // 必须在后台线程执行
​(2)异步准备​ ​:prepareAsync()

这是推荐的方式。它会立即返回,并在后台准备媒体。你需要设置一个监听器来接收准备完成的通知。

Kotlin 复制代码
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mp) {
        // 媒体准备就绪,可以开始播放了
        mediaPlayer.start();
    }
});
mediaPlayer.prepareAsync(); // 非阻塞,安全在主线程调用

4. 开始播放​

Kotlin 复制代码
mediaPlayer.start(); // 开始或恢复播放

5. ​​管理播放状态​

Kotlin 复制代码
mediaPlayer.pause();   // 暂停播放
mediaPlayer.stop();    // 停止播放,之后需要重新 prepare 才能再次 start
mediaPlayer.seekTo(10000); // 跳转到第10秒(单位:毫秒)

6. 释放资源(非常重要!)​

MediaPlayer 会占用系统稀缺的资源(如编解码器)。当不再使用时,​​必须​​释放它。

Kotlin 复制代码
mediaPlayer.release();
mediaPlayer = null;

通常这个操作在 Activity 的 onDestroy()方法中执行。


(三)MediaPlayer 的生命周期

理解 MediaPlayer 的状态机至关重要,可以避免很多错误(例如在错误的状态下调用方法)。其生命周期可以简化为以下核心状态:

  1. ​Idle(空闲)​ ​: 通过 new创建后的状态。此时尚未设置数据源。

  2. ​Initialized(已初始化)​ ​: 调用 setDataSource()后的状态。

  3. ​Preparing / Prepared(准备中/已就绪)​​:

    • 调用 prepareAsync()后进入 ​​Preparing​​ 状态。

    • 准备完成后(onPrepared被调用)进入 ​​Prepared​ ​ 状态。此时可以调用 start()

  4. ​Started(已开始)​ ​: 调用 start()后,媒体正在播放。可以调用 isPlaying()来检查。

  5. ​Paused(已暂停)​ ​: 调用 pause()后的状态。可以从这里恢复播放。

  6. ​Stopped(已停止)​ ​: 调用 stop()后的状态。要重新播放,必须再次调用 prepare()prepareAsync()

  7. ​PlaybackCompleted(播放完成)​ ​: 媒体播放完毕。可以设置循环播放(setLooping(true))或通过 OnCompletionListener来监听。

  8. ​Error(错误)​ ​: 发生错误时进入此状态。应该设置 OnErrorListener来处理错误。

  9. ​End(结束)​ ​: 调用 release()后进入此状态。MediaPlayer 不再可用。

​关键点​ ​:从 Stopped状态重新开始,必须再次 prepare。调用 reset()方法可以将 MediaPlayer 重置到 Idle状态。


(三)完整代码示例

Kotlin 复制代码
public class MainActivity extends AppCompatActivity {

    private MediaPlayer mediaPlayer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button playButton = findViewById(R.id.play_button);
        Button pauseButton = findViewById(R.id.pause_button);

        // 1. 使用 create 方法创建,它已经同步准备好了资源,无需再调用 prepare()
        mediaPlayer = MediaPlayer.create(this, R.raw.my_sound);

        // 设置播放完成监听
        mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                // 播放结束,释放资源
                releaseMediaPlayer();
                Toast.makeText(MainActivity.this, "播放完成", Toast.LENGTH_SHORT).show();
            }
        });

        playButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
                    mediaPlayer.start();
                }
            }
        });

        pauseButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mediaPlayer != null && mediaPlayer.isPlaying()) {
                    mediaPlayer.pause();
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 确保在 Activity 销毁时释放 MediaPlayer
        releaseMediaPlayer();
    }

    /**
     * 释放 MediaPlayer 资源的辅助方法
     */
    private void releaseMediaPlayer() {
        if (mediaPlayer != null) {
            mediaPlayer.release();
            mediaPlayer = null;
        }
    }
}

(四)最佳实践和注意事项

  1. ​及时释放资源​ ​:这是最重要的原则。在 Activity/Fragment 的 onPause()onDestroy()中释放 MediaPlayer。否则会导致内存泄漏和资源占用。

  2. ​使用异步准备​ ​:对于本地大文件或网络流,始终使用 prepareAsync()OnPreparedListener,避免阻塞 UI 线程。

  3. ​处理音频焦点(Audio Focus)​ ​:在播放音频前,申请音频焦点。当有来电或其他应用播放声音时,根据音频焦点的变化做出相应处理(如暂停播放、降低音量),这是良好的用户体验。这需要用到 AudioManager

  4. ​处理错误​ ​:务必设置 OnErrorListener,以便在媒体播放出错时能做出响应(如给用户提示)。

    Kotlin 复制代码
    mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
        @Override
        public boolean onError(MediaPlayer mp, int what, int extra) {
            // 'what' 和 'extra' 是错误代码
            Log.e("MediaPlayer", "Error: " + what + ", " + extra);
            releaseMediaPlayer();
            return true; // 返回 true 表示错误已处理
        }
    });
  5. ​使用 reset() ​:如果你想用同一个 MediaPlayer 实例播放另一个数据源,先调用 reset()将其重置到 Idle状态,然后再设置新的数据源并 prepare


二. SoundPool

(一)介绍

SoundPool 是 Android 中用于播放短音频片段的类,特别适合播放需要低延迟、快速响应的音效(如游戏音效、按键声等)。


(二)特点

  • 低延迟播放:适合短音效(通常不超过 5 秒)

  • 同时播放多个音频:可管理多个音频流

  • 预加载机制:提前加载音频到内存,减少播放延迟

  • 资源高效:适合频繁播放的小音频


(三)基本使用步骤

1. 创建 SoundPool 对象

java 复制代码
// 使用 SoundPool.Builder
AudioAttributes attributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_GAME)           // 使用场景
        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) // 内容类型
        .build();

SoundPool soundPool = new SoundPool.Builder()
        .setAudioAttributes(attributes)
        .setMaxStreams(10)  // 最大同时播放流数
        .build();

2. 加载音频资源

java 复制代码
// 从资源文件加载
int soundId1 = soundPool.load(this, R.raw.beep, 1);

// 从文件路径加载
int soundId2 = soundPool.load("/sdcard/sound/click.wav", 1);

// 从Asset加载
try {
    AssetFileDescriptor afd = getAssets().openFd("sounds/explosion.wav");
    int soundId3 = soundPool.load(afd, 1);
} catch (IOException e) {
    e.printStackTrace();
}

3. 设置加载完成监听

java 复制代码
soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
    @Override
    public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
        if (status == 0) {
            // 加载成功,sampleId 是对应音频的ID
            Log.d("SoundPool", "音频加载成功,ID: " + sampleId);
        } else {
            Log.e("SoundPool", "音频加载失败");
        }
    }
});

4. 播放音频

java 复制代码
// 播放音频
int streamId = soundPool.play(
    soundId,    // 音频ID
    1.0f,       // 左声道音量 (0.0 - 1.0)
    1.0f,       // 右声道音量
    1,          // 优先级 (0 = 最低)
    0,          // 循环次数 (0 = 不循环, -1 = 无限循环)
    1.0f        // 播放速率 (0.5 - 2.0)
);

5. 控制播放

java 复制代码
// 暂停播放
soundPool.pause(streamId);

// 恢复播放
soundPool.resume(streamId);

// 停止播放
soundPool.stop(streamId);

// 设置循环(-1无限循环,0不循环,n循环n次)
soundPool.setLoop(streamId, 2);

// 设置音量
soundPool.setVolume(streamId, 0.5f, 0.8f);

// 设置播放速率
soundPool.setRate(streamId, 1.5f); // 1.5倍速

6. 资源释放

java 复制代码
// 释放单个音频
soundPool.unload(soundId);

// 释放所有资源
soundPool.release();
soundPool = null;

(四) 注意事项

**音频格式限制:**建议使用 OGG 或 WAV 格式,避免使用 MP3(可能有兼容性问题)

音频长度: 适合短音频(1-5秒),长音频建议使用 MediaPlayer
内存管理: 不要加载过多或过大的音频文件
生命周期管理: 在 Activity 的 onDestroy() 中释放资源
**异步加载:**音频加载是异步的,播放前确保已加载完成


三. VideoView

(一)介绍

VideoView 是 Android 提供的用于播放视频的视图组件,它封装了 MediaPlayer 和 SurfaceView,提供了简单的视频播放功能。


(二)特点

  • 简单易用:无需复杂配置即可实现视频播放

  • 内置控制界面:支持播放/暂停、进度条等控制功能

  • 支持多种格式:可播放本地和网络视频

  • 生命周期管理:可与 Activity/Fragment 生命周期绑定


(三)基本使用步骤

1. 布局文件中添加 VideoView

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="300dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btnPlay"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="播放" />

        <Button
            android:id="@+id/btnPause"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="暂停" />

        <Button
            android:id="@+id/btnReplay"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="重播" />
    </LinearLayout>

</LinearLayout>

2. 基本配置和播放控制

java 复制代码
public class VideoPlayerActivity extends AppCompatActivity {
    private VideoView videoView;
    private MediaController mediaController;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video_player);
        
        initVideoView();
        setupControls();
    }
    
    private void initVideoView() {
        videoView = findViewById(R.id.videoView);
        
        // 设置媒体控制器(内置控制界面)
        mediaController = new MediaController(this);
        videoView.setMediaController(mediaController);
        
        // 设置视频路径
        String videoPath = "android.resource://" + getPackageName() + "/" + R.raw.sample_video;
        Uri uri = Uri.parse(videoPath);
        videoView.setVideoURI(uri);
        
        // 或者从文件路径设置
        // String filePath = Environment.getExternalStorageDirectory() + "/Movies/sample.mp4";
        // videoView.setVideoPath(filePath);
        
        // 或者从网络URL设置
        // String videoUrl = "https://example.com/sample.mp4";
        // videoView.setVideoURI(Uri.parse(videoUrl));
    }
    
    private void setupControls() {
        Button btnPlay = findViewById(R.id.btnPlay);
        Button btnPause = findViewById(R.id.btnPause);
        Button btnReplay = findViewById(R.id.btnReplay);
        
        btnPlay.setOnClickListener(v -> videoView.start());
        btnPause.setOnClickListener(v -> videoView.pause());
        btnReplay.setOnClickListener(v -> {
            videoView.resume();
            videoView.start();
        });
    }
}

(四)高级功能实现

1. 自定义视频播放器

java 复制代码
public class CustomVideoPlayer {
    private VideoView videoView;
    private SeekBar seekBar;
    private TextView currentTime, totalTime;
    private Handler handler = new Handler();
    
    public CustomVideoPlayer(VideoView videoView, SeekBar seekBar, 
                            TextView currentTime, TextView totalTime) {
        this.videoView = videoView;
        this.seekBar = seekBar;
        this.currentTime = currentTime;
        this.totalTime = totalTime;
        
        setupSeekBar();
        setupListeners();
    }
    
    private void setupSeekBar() {
        // 更新进度条的Runnable
        Runnable updateProgress = new Runnable() {
            @Override
            public void run() {
                if (videoView.isPlaying()) {
                    int current = videoView.getCurrentPosition();
                    int duration = videoView.getDuration();
                    
                    seekBar.setProgress(current);
                    seekBar.setMax(duration);
                    
                    currentTime.setText(formatTime(current));
                    totalTime.setText(formatTime(duration));
                }
                handler.postDelayed(this, 1000);
            }
        };
        handler.post(updateProgress);
        
        //  SeekBar变化监听
        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (fromUser) {
                    videoView.seekTo(progress);
                }
            }
            
            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {}
            
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {}
        });
    }
    
    private void setupListeners() {
        // 准备完成监听
        videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                // 视频准备完成,可以开始播放
                seekBar.setMax(videoView.getDuration());
                totalTime.setText(formatTime(videoView.getDuration()));
            }
        });
        
        // 播放完成监听
        videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                // 播放完成后的操作
                videoView.seekTo(0);
                seekBar.setProgress(0);
            }
        });
        
        // 错误监听
        videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
            @Override
            public boolean onError(MediaPlayer mp, int what, int extra) {
                // 处理播放错误
                Log.e("VideoView", "播放错误: " + what + ", " + extra);
                return true;
            }
        });
    }
    
    private String formatTime(int milliseconds) {
        int seconds = milliseconds / 1000;
        int minutes = seconds / 60;
        seconds = seconds % 60;
        return String.format("%02d:%02d", minutes, seconds);
    }
    
    public void release() {
        handler.removeCallbacksAndMessages(null);
    }
}

2. 完整的视频播放器 Activity

java 复制代码
public class AdvancedVideoPlayerActivity extends AppCompatActivity {
    private VideoView videoView;
    private SeekBar seekBar;
    private TextView currentTime, totalTime;
    private Button btnPlayPause;
    private CustomVideoPlayer customPlayer;
    private boolean isPlaying = false;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_advanced_video_player);
        
        initViews();
        setupVideoPlayer();
    }
    
    private void initViews() {
        videoView = findViewById(R.id.videoView);
        seekBar = findViewById(R.id.seekBar);
        currentTime = findViewById(R.id.currentTime);
        totalTime = findViewById(R.id.totalTime);
        btnPlayPause = findViewById(R.id.btnPlayPause);
        
        // 隐藏默认控制器
        videoView.setMediaController(null);
    }
    
    private void setupVideoPlayer() {
        // 初始化自定义播放器
        customPlayer = new CustomVideoPlayer(videoView, seekBar, currentTime, totalTime);
        
        // 设置视频源
        String videoUrl = getIntent().getStringExtra("video_url");
        if (videoUrl != null) {
            videoView.setVideoURI(Uri.parse(videoUrl));
        } else {
            // 默认视频
            Uri uri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.sample_video);
            videoView.setVideoURI(uri);
        }
        
        // 播放/暂停按钮
        btnPlayPause.setOnClickListener(v -> togglePlayPause());
        
        // 全屏按钮
        findViewById(R.id.btnFullscreen).setOnClickListener(v -> toggleFullscreen());
    }
    
    private void togglePlayPause() {
        if (isPlaying) {
            videoView.pause();
            btnPlayPause.setText("播放");
        } else {
            videoView.start();
            btnPlayPause.setText("暂停");
        }
        isPlaying = !isPlaying;
    }
    
    private void toggleFullscreen() {
        if (getSupportActionBar() != null) {
            if (getSupportActionBar().isShowing()) {
                getSupportActionBar().hide();
                getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            } else {
                getSupportActionBar().show();
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            }
        }
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        if (videoView.isPlaying()) {
            videoView.pause();
        }
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        if (isPlaying) {
            videoView.start();
        }
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (customPlayer != null) {
            customPlayer.release();
        }
        videoView.stopPlayback();
    }
}

3. 对应的布局文件

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:layout_alignParentTop="true" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/videoView"
        android:orientation="vertical"
        android:padding="16dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/currentTime"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="00:00" />

            <SeekBar
                android:id="@+id/seekBar"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:layout_marginHorizontal="8dp" />

            <TextView
                android:id="@+id/totalTime"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="00:00" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginTop="16dp">

            <Button
                android:id="@+id/btnPlayPause"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="播放" />

            <Button
                android:id="@+id/btnFullscreen"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="全屏" />
        </LinearLayout>

    </LinearLayout>

</RelativeLayout>

(五)网络视频播放配置

1. 添加网络权限

XML 复制代码
<uses-permission android:name="android.permission.INTERNET" />

2. 处理网络视频缓冲

java 复制代码
videoView.setOnInfoListener(new MediaPlayer.OnInfoListener() {
    @Override
    public boolean onInfo(MediaPlayer mp, int what, int extra) {
        switch (what) {
            case MediaPlayer.MEDIA_INFO_BUFFERING_START:
                // 显示加载提示
                showLoading();
                return true;
            case MediaPlayer.MEDIA_INFO_BUFFERING_END:
                // 隐藏加载提示
                hideLoading();
                return true;
            case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
                // 视频开始渲染
                return true;
        }
        return false;
    }
});

(六)注意事项

1. 权限管理

  • 网络视频需要 INTERNET权限

  • 本地文件需要存储权限

2. 格式支持

  • 支持 MP4、3GP 等常见格式

  • 不同设备支持的编码格式可能不同

3. 内存管理

  • 及时释放资源,避免内存泄漏

  • 在 onPause() 中暂停播放,onDestroy() 中释放资源

4. 网络视频

  • 使用 HTTPS 确保安全性

  • 处理网络异常情况

5. 生命周期

  • 正确处理 Activity 生命周期

  • 配置变化时保存播放状态


四. MediaRecorder

(一)介绍

MediaRecorder 是 Android 中用于录制音频和视频的核心类,它提供了完整的媒体录制功能,支持音频、视频的采集、编码和保存。


(二)特点

  • 多功能录制:支持音频、视频录制

  • 格式丰富:支持多种编码格式和输出格式

  • 灵活配置:可自定义录制参数(分辨率、比特率、帧率等)

  • 状态机管理:严格的状态转换控制


(三)状态机

MediaRecorder 遵循严格的状态转换流程:

Initial → Initialized → DataSourceConfigured → Prepared → Recording → Released


(四)基本使用步骤

1. 权限配置

XML 复制代码
<!-- 音频录制权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />

<!-- 视频录制权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

<!-- 存储权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<!-- 如果针对 Android 10+ 使用媒体存储 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

2. 音频录制基础实现

java 复制代码
public class AudioRecorder {
    private MediaRecorder mediaRecorder;
    private String outputFile;
    private boolean isRecording = false;
    
    public AudioRecorder(Context context) {
        mediaRecorder = new MediaRecorder();
        // 设置输出文件路径
        outputFile = getOutputFilePath(context, "audio");
    }
    
    private String getOutputFilePath(Context context, String type) {
        File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES);
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
        String fileName = type + "_" + timeStamp;
        
        if (type.equals("audio")) {
            return storageDir.getAbsolutePath() + "/" + fileName + ".3gp";
        } else {
            return storageDir.getAbsolutePath() + "/" + fileName + ".mp4";
        }
    }
    
    public void startAudioRecording() {
        try {
            // 重置 MediaRecorder(如果是重复使用)
            mediaRecorder.reset();
            
            // 设置音频源(麦克风)
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            
            // 设置输出格式
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
            
            // 设置音频编码器
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
            
            // 设置输出文件
            mediaRecorder.setOutputFile(outputFile);
            
            // 准备录制
            mediaRecorder.prepare();
            
            // 开始录制
            mediaRecorder.start();
            isRecording = true;
            
            Log.d("AudioRecorder", "开始录制音频: " + outputFile);
            
        } catch (IOException e) {
            Log.e("AudioRecorder", "录制准备失败: " + e.getMessage());
            e.printStackTrace();
        } catch (IllegalStateException e) {
            Log.e("AudioRecorder", "状态异常: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    public void stopAudioRecording() {
        if (isRecording) {
            try {
                mediaRecorder.stop();
                mediaRecorder.reset(); // 重置到初始状态
                isRecording = false;
                Log.d("AudioRecorder", "停止录制,文件保存至: " + outputFile);
            } catch (RuntimeException e) {
                Log.e("AudioRecorder", "停止录制失败: " + e.getMessage());
                // 可能是录制时间太短导致的异常
            }
        }
    }
    
    public void release() {
        if (mediaRecorder != null) {
            mediaRecorder.release();
            mediaRecorder = null;
        }
    }
    
    public boolean isRecording() {
        return isRecording;
    }
    
    public String getOutputFile() {
        return outputFile;
    }
}

3. 视频录制基础实现

java 复制代码
public class VideoRecorder {
    private MediaRecorder mediaRecorder;
    private Camera camera;
    private String outputFile;
    private boolean isRecording = false;
    private SurfaceHolder surfaceHolder;
    
    public VideoRecorder(Context context, SurfaceHolder holder) {
        this.surfaceHolder = holder;
        mediaRecorder = new MediaRecorder();
        outputFile = getOutputFilePath(context, "video");
    }
    
    public void startVideoRecording() {
        try {
            // 释放相机(如果已占用)
            if (camera != null) {
                camera.unlock();
            }
            
            mediaRecorder.reset();
            
            // 设置视频源(相机)
            mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
            
            // 设置音频源
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
            
            // 设置输出格式
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            
            // 设置视频编码器
            mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
            
            // 设置音频编码器
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            
            // 设置视频参数
            mediaRecorder.setVideoSize(1280, 720);  // 分辨率
            mediaRecorder.setVideoFrameRate(30);    // 帧率
            mediaRecorder.setVideoEncodingBitRate(3000000); // 比特率 3Mbps
            
            // 设置预览显示
            mediaRecorder.setPreviewDisplay(surfaceHolder.getSurface());
            
            // 设置输出文件
            mediaRecorder.setOutputFile(outputFile);
            
            // 设置方向提示(如果需要)
            mediaRecorder.setOrientationHint(90); // 竖屏90度
            
            mediaRecorder.prepare();
            mediaRecorder.start();
            isRecording = true;
            
            Log.d("VideoRecorder", "开始录制视频: " + outputFile);
            
        } catch (IOException e) {
            Log.e("VideoRecorder", "录制准备失败: " + e.getMessage());
            e.printStackTrace();
        } catch (IllegalStateException e) {
            Log.e("VideoRecorder", "状态异常: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    public void stopVideoRecording() {
        if (isRecording) {
            try {
                mediaRecorder.stop();
                mediaRecorder.reset();
                isRecording = false;
                
                // 重新锁定相机(如果使用)
                if (camera != null) {
                    camera.lock();
                }
                
                Log.d("VideoRecorder", "停止录制,文件保存至: " + outputFile);
            } catch (RuntimeException e) {
                Log.e("VideoRecorder", "停止录制失败: " + e.getMessage());
            }
        }
    }
    
    public void setCamera(Camera camera) {
        this.camera = camera;
    }
    
    public void release() {
        if (mediaRecorder != null) {
            mediaRecorder.release();
            mediaRecorder = null;
        }
        if (camera != null) {
            camera.release();
            camera = null;
        }
    }
    
    public boolean isRecording() {
        return isRecording;
    }
    
    private String getOutputFilePath(Context context, String type) {
        // 同上...
    }
}

(五)高级功能实现

1. 完整的录制管理器

java 复制代码
public class MediaRecorderManager {
    private MediaRecorder mediaRecorder;
    private Camera camera;
    private String outputFile;
    private boolean isRecording = false;
    private RecorderType currentType;
    private MediaRecorderCallback callback;
    
    public enum RecorderType {
        AUDIO, VIDEO
    }
    
    public interface MediaRecorderCallback {
        void onRecordingStarted();
        void onRecordingStopped(String filePath);
        void onError(String error);
        void onRecordingTimeUpdate(long milliseconds);
    }
    
    public MediaRecorderManager() {
        mediaRecorder = new MediaRecorder();
    }
    
    public void setupAudioRecorder(Context context) {
        currentType = RecorderType.AUDIO;
        outputFile = getOutputFilePath(context, "audio");
        
        try {
            mediaRecorder.reset();
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            mediaRecorder.setAudioEncodingBitRate(128000); // 128kbps
            mediaRecorder.setAudioSamplingRate(44100);     // 44.1kHz
            mediaRecorder.setOutputFile(outputFile);
            mediaRecorder.prepare();
            
        } catch (Exception e) {
            if (callback != null) {
                callback.onError("音频录制设置失败: " + e.getMessage());
            }
        }
    }
    
    public void setupVideoRecorder(Context context, Camera camera, SurfaceHolder holder) {
        currentType = RecorderType.VIDEO;
        this.camera = camera;
        outputFile = getOutputFilePath(context, "video");
        
        try {
            if (camera != null) {
                camera.unlock();
            }
            
            mediaRecorder.reset();
            mediaRecorder.setCamera(camera);
            mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            
            // 高质量视频设置
            mediaRecorder.setVideoSize(1920, 1080);
            mediaRecorder.setVideoFrameRate(30);
            mediaRecorder.setVideoEncodingBitRate(6000000); // 6Mbps
            mediaRecorder.setAudioEncodingBitRate(128000);
            
            // 预览设置
            if (holder != null) {
                mediaRecorder.setPreviewDisplay(holder.getSurface());
            }
            
            mediaRecorder.setOutputFile(outputFile);
            mediaRecorder.setOrientationHint(90);
            mediaRecorder.prepare();
            
        } catch (Exception e) {
            if (callback != null) {
                callback.onError("视频录制设置失败: " + e.getMessage());
            }
        }
    }
    
    public void startRecording() {
        if (isRecording) {
            return;
        }
        
        try {
            mediaRecorder.start();
            isRecording = true;
            
            if (callback != null) {
                callback.onRecordingStarted();
            }
            
            // 开始计时(可选)
            startTimer();
            
        } catch (IllegalStateException e) {
            if (callback != null) {
                callback.onError("开始录制失败: " + e.getMessage());
            }
        }
    }
    
    public void stopRecording() {
        if (!isRecording) {
            return;
        }
        
        try {
            mediaRecorder.stop();
            isRecording = false;
            stopTimer();
            
            if (callback != null) {
                callback.onRecordingStopped(outputFile);
            }
            
            // 重置以备下次使用
            mediaRecorder.reset();
            
            if (camera != null) {
                camera.lock();
            }
            
        } catch (RuntimeException e) {
            if (callback != null) {
                callback.onError("停止录制失败: " + e.getMessage());
            }
        }
    }
    
    private Handler timerHandler = new Handler();
    private long startTime = 0;
    private Runnable timerRunnable = new Runnable() {
        @Override
        public void run() {
            if (isRecording && callback != null) {
                long elapsed = System.currentTimeMillis() - startTime;
                callback.onRecordingTimeUpdate(elapsed);
                timerHandler.postDelayed(this, 1000);
            }
        }
    };
    
    private void startTimer() {
        startTime = System.currentTimeMillis();
        timerHandler.postDelayed(timerRunnable, 1000);
    }
    
    private void stopTimer() {
        timerHandler.removeCallbacks(timerRunnable);
    }
    
    public void setCallback(MediaRecorderCallback callback) {
        this.callback = callback;
    }
    
    public void release() {
        stopRecording();
        if (mediaRecorder != null) {
            mediaRecorder.release();
            mediaRecorder = null;
        }
        if (camera != null) {
            camera.release();
            camera = null;
        }
        stopTimer();
    }
    
    // 其他getter方法...
    private String getOutputFilePath(Context context, String type) {
        // 实现同上...
    }
}

2. 完整的录制 Activity 实现

java 复制代码
public class RecordingActivity extends AppCompatActivity {
    private MediaRecorderManager recorderManager;
    private SurfaceView surfaceView;
    private Camera camera;
    
    private Button btnRecord;
    private TextView tvTimer;
    private boolean hasCameraPermission = false;
    private boolean hasAudioPermission = false;
    
    private static final int PERMISSION_REQUEST_CODE = 100;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recording);
        
        initViews();
        checkPermissions();
        initRecorderManager();
    }
    
    private void initViews() {
        surfaceView = findViewById(R.id.surfaceView);
        btnRecord = findViewById(R.id.btnRecord);
        tvTimer = findViewById(R.id.tvTimer);
        
        btnRecord.setOnClickListener(v -> toggleRecording());
        
        // 设置SurfaceHolder
        SurfaceHolder holder = surfaceView.getHolder();
        holder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                initCamera();
            }
            
            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
            
            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                releaseCamera();
            }
        });
    }
    
    private void initCamera() {
        if (hasCameraPermission) {
            try {
                camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
                Camera.Parameters params = camera.getParameters();
                // 设置相机参数...
                camera.setDisplayOrientation(90);
                camera.setPreviewDisplay(surfaceView.getHolder());
                camera.startPreview();
            } catch (Exception e) {
                Log.e("RecordingActivity", "相机初始化失败: " + e.getMessage());
            }
        }
    }
    
    private void initRecorderManager() {
        recorderManager = new MediaRecorderManager();
        recorderManager.setCallback(new MediaRecorderManager.MediaRecorderCallback() {
            @Override
            public void onRecordingStarted() {
                runOnUiThread(() -> {
                    btnRecord.setText("停止录制");
                    btnRecord.setBackgroundColor(Color.RED);
                });
            }
            
            @Override
            public void onRecordingStopped(String filePath) {
                runOnUiThread(() -> {
                    btnRecord.setText("开始录制");
                    btnRecord.setBackgroundColor(Color.GREEN);
                    tvTimer.setText("00:00");
                    
                    // 显示录制完成提示
                    Toast.makeText(RecordingActivity.this, 
                        "录制完成: " + filePath, Toast.LENGTH_LONG).show();
                });
            }
            
            @Override
            public void onError(String error) {
                runOnUiThread(() -> {
                    Toast.makeText(RecordingActivity.this, error, Toast.LENGTH_LONG).show();
                });
            }
            
            @Override
            public void onRecordingTimeUpdate(long milliseconds) {
                runOnUiThread(() -> {
                    tvTimer.setText(formatTime(milliseconds));
                });
            }
        });
    }
    
    private void toggleRecording() {
        if (recorderManager.isRecording()) {
            recorderManager.stopRecording();
        } else {
            if (hasCameraPermission && hasAudioPermission) {
                // 设置视频录制
                recorderManager.setupVideoRecorder(this, camera, surfaceView.getHolder());
                recorderManager.startRecording();
            }
        }
    }
    
    private String formatTime(long milliseconds) {
        long seconds = milliseconds / 1000;
        long minutes = seconds / 60;
        seconds = seconds % 60;
        return String.format("%02d:%02d", minutes, seconds);
    }
    
    private void checkPermissions() {
        String[] permissions = {
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        };
        
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) 
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE);
        } else {
            hasCameraPermission = true;
            hasAudioPermission = true;
            initCamera();
        }
    }
    
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == PERMISSION_REQUEST_CODE) {
            hasCameraPermission = grantResults[0] == PackageManager.PERMISSION_GRANTED;
            hasAudioPermission = grantResults[1] == PackageManager.PERMISSION_GRANTED;
            
            if (hasCameraPermission) {
                initCamera();
            } else {
                Toast.makeText(this, "需要相机权限才能录制视频", Toast.LENGTH_LONG).show();
            }
        }
    }
    
    private void releaseCamera() {
        if (camera != null) {
            camera.stopPreview();
            camera.release();
            camera = null;
        }
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        if (recorderManager != null && recorderManager.isRecording()) {
            recorderManager.stopRecording();
        }
        releaseCamera();
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (recorderManager != null) {
            recorderManager.release();
        }
    }
}

(六)对应布局文件

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- 预览画面 -->
    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <!-- 控制区域 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="16dp"
        android:gravity="center">

        <TextView
            android:id="@+id/tvTimer"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="00:00"
            android:textSize="18sp"
            android:textStyle="bold"
            android:gravity="center" />

        <Button
            android:id="@+id/btnRecord"
            android:layout_width="120dp"
            android:layout_height="120dp"
            android:text="开始录制"
            android:textSize="16sp"
            android:background="@drawable/record_button_shape" />

        <View
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

    </LinearLayout>

</LinearLayout>

(七)录制按钮样式

XML 复制代码
<!-- res/drawable/record_button_shape.xml -->
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="#4CAF50" />
    <stroke
        android:width="2dp"
        android:color="#FFFFFF" />
    <size
        android:width="120dp"
        android:height="120dp" />
</shape>

(八)注意事项

  • 权限管理:确保在运行时请求必要权限
  • 状态管理:严格遵守 MediaRecorder 的状态转换顺序
  • 异常处理:妥善处理各种异常情况
  • 资源释放:及时释放 MediaRecorder 和 Camera 资源
  • 文件管理:使用适当的文件路径和命名规则
  • 性能优化:根据设备能力调整录制参数
  • Android 10+ 适配:使用分区存储或 MediaStore API

五. AudioRecord

(一)介绍

AudioRecord 是 Android 中用于直接录制原始音频数据的类,它提供了比 MediaRecorder 更底层的音频录制控制,适合需要实时处理音频数据的场景。


(二)特点

  • 原始音频数据:直接获取 PCM 音频数据

  • 实时处理:适合音频分析、实时处理等场景

  • 低延迟:提供更低的音频录制延迟

  • 灵活控制:可自定义音频参数和数据处理逻辑


(三)核心参数

1. 音频参数说明

  • 采样率(Sample Rate):8000Hz、16000Hz、44100Hz 等

  • 声道配置(Channel Config):单声道(MONO)、立体声(STEREO)

  • 音频格式(Audio Format):8bit、16bit PCM

2. 缓冲区计算

java 复制代码
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

(四)基本使用步骤

1. 权限配置

XML 复制代码
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

2. 基础 AudioRecord 实现

java 复制代码
public class BasicAudioRecorder {
    private static final String TAG = "BasicAudioRecorder";
    
    // 音频参数
    private static final int SAMPLE_RATE = 44100;        // 44.1kHz
    private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; // 单声道
    private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; // 16bit PCM
    
    private AudioRecord audioRecord;
    private int bufferSize;
    private boolean isRecording = false;
    private Thread recordingThread;
    
    public boolean initAudioRecorder() {
        // 获取最小缓冲区大小
        bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT);
        
        if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) {
            Log.e(TAG, "无法获取有效的缓冲区大小");
            return false;
        }
        
        try {
            // 创建 AudioRecord 实例
            audioRecord = new AudioRecord(
                MediaRecorder.AudioSource.MIC,    // 音频源(麦克风)
                SAMPLE_RATE,
                CHANNEL_CONFIG,
                AUDIO_FORMAT,
                bufferSize * 2                   // 缓冲区大小(最小值的2倍)
            );
            
            if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
                Log.e(TAG, "AudioRecord 初始化失败");
                return false;
            }
            
            return true;
            
        } catch (SecurityException e) {
            Log.e(TAG, "没有录音权限: " + e.getMessage());
            return false;
        }
    }
    
    public void startRecording() {
        if (isRecording || audioRecord == null) {
            return;
        }
        
        try {
            audioRecord.startRecording();
            isRecording = true;
            
            // 创建录音线程
            recordingThread = new Thread(new RecordingRunnable(), "AudioRecorder Thread");
            recordingThread.start();
            
            Log.d(TAG, "开始录制音频");
            
        } catch (IllegalStateException e) {
            Log.e(TAG, "启动录制失败: " + e.getMessage());
        }
    }
    
    public void stopRecording() {
        if (!isRecording || audioRecord == null) {
            return;
        }
        
        isRecording = false;
        
        try {
            if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                audioRecord.stop();
            }
            
            // 等待录音线程结束
            if (recordingThread != null) {
                recordingThread.join(1000);
            }
            
            Log.d(TAG, "停止录制音频");
            
        } catch (InterruptedException e) {
            Log.e(TAG, "停止录制时线程中断: " + e.getMessage());
            Thread.currentThread().interrupt();
        }
    }
    
    public void release() {
        stopRecording();
        
        if (audioRecord != null) {
            audioRecord.release();
            audioRecord = null;
        }
    }
    
    private class RecordingRunnable implements Runnable {
        @Override
        public void run() {
            // 创建缓冲区
            byte[] buffer = new byte[bufferSize];
            
            while (isRecording && audioRecord != null) {
                // 读取音频数据
                int bytesRead = audioRecord.read(buffer, 0, buffer.length);
                
                if (bytesRead > 0) {
                    // 处理音频数据(这里只是示例)
                    processAudioData(buffer, bytesRead);
                } else {
                    Log.e(TAG, "读取音频数据错误: " + bytesRead);
                    break;
                }
            }
        }
        
        private void processAudioData(byte[] data, int length) {
            // 这里可以添加音频数据处理逻辑
            // 例如:实时分析、保存到文件、网络传输等
            Log.d(TAG, "处理音频数据长度: " + length + " 字节");
        }
    }
    
    public boolean isRecording() {
        return isRecording;
    }
    
    public int getAudioSessionId() {
        return audioRecord != null ? audioRecord.getAudioSessionId() : -1;
    }
}

(五)高级功能实现

1. 完整的音频录制管理器

java 复制代码
public class AdvancedAudioRecorder {
    private static final String TAG = "AdvancedAudioRecorder";
    
    // 支持的音频参数
    public enum AudioQuality {
        LOW(8000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT),
        MEDIUM(16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT),
        HIGH(44100, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
        
        final int sampleRate;
        final int channelConfig;
        final int audioFormat;
        
        AudioQuality(int sampleRate, int channelConfig, int audioFormat) {
            this.sampleRate = sampleRate;
            this.channelConfig = channelConfig;
            this.audioFormat = audioFormat;
        }
    }
    
    private AudioRecord audioRecord;
    private int bufferSize;
    private boolean isRecording = false;
    private Thread recordingThread;
    private AudioQuality currentQuality = AudioQuality.HIGH;
    private AudioRecorderCallback callback;
    private File outputFile;
    private FileOutputStream fileOutputStream;
    
    public interface AudioRecorderCallback {
        void onRecordingStarted();
        void onRecordingStopped(File outputFile);
        void onError(String error);
        void onAudioDataAvailable(byte[] data, int size);
        void onRecordingProgress(long duration);
    }
    
    public AdvancedAudioRecorder(AudioQuality quality) {
        this.currentQuality = quality;
    }
    
    public boolean initialize() {
        // 计算缓冲区大小
        bufferSize = AudioRecord.getMinBufferSize(
            currentQuality.sampleRate,
            currentQuality.channelConfig,
            currentQuality.audioFormat
        );
        
        if (bufferSize <= 0) {
            notifyError("无效的缓冲区大小: " + bufferSize);
            return false;
        }
        
        try {
            audioRecord = new AudioRecord(
                MediaRecorder.AudioSource.MIC,
                currentQuality.sampleRate,
                currentQuality.channelConfig,
                currentQuality.audioFormat,
                bufferSize * 2
            );
            
            if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
                notifyError("AudioRecord 初始化失败");
                return false;
            }
            
            Log.d(TAG, String.format("AudioRecord 初始化成功 - 采样率: %d, 声道: %s, 格式: %s",
                currentQuality.sampleRate,
                getChannelConfigName(currentQuality.channelConfig),
                getAudioFormatName(currentQuality.audioFormat)));
            
            return true;
            
        } catch (Exception e) {
            notifyError("初始化异常: " + e.getMessage());
            return false;
        }
    }
    
    public void startRecording(Context context) {
        if (isRecording || audioRecord == null) {
            return;
        }
        
        try {
            // 创建输出文件
            outputFile = createOutputFile(context, "pcm");
            fileOutputStream = new FileOutputStream(outputFile);
            
            audioRecord.startRecording();
            isRecording = true;
            
            // 开始录音线程
            recordingThread = new Thread(new RecordingRunnable(), "AdvancedAudioRecorder");
            recordingThread.start();
            
            notifyRecordingStarted();
            Log.d(TAG, "开始录制音频到: " + outputFile.getAbsolutePath());
            
        } catch (Exception e) {
            notifyError("开始录制失败: " + e.getMessage());
        }
    }
    
    public void stopRecording() {
        if (!isRecording) {
            return;
        }
        
        isRecording = false;
        
        try {
            if (audioRecord != null && audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                audioRecord.stop();
            }
            
            // 等待录音线程结束
            if (recordingThread != null) {
                recordingThread.join(2000);
            }
            
            // 关闭文件流
            if (fileOutputStream != null) {
                fileOutputStream.close();
                fileOutputStream = null;
            }
            
            notifyRecordingStopped();
            Log.d(TAG, "音频录制完成: " + outputFile.getAbsolutePath());
            
        } catch (Exception e) {
            notifyError("停止录制异常: " + e.getMessage());
        }
    }
    
    private class RecordingRunnable implements Runnable {
        private long startTime = 0;
        
        @Override
        public void run() {
            android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
            
            byte[] buffer = new byte[bufferSize];
            startTime = System.currentTimeMillis();
            
            while (isRecording && audioRecord != null) {
                int bytesRead = audioRecord.read(buffer, 0, buffer.length);
                
                if (bytesRead > 0) {
                    // 通知数据可用
                    notifyAudioDataAvailable(buffer, bytesRead);
                    
                    // 写入文件
                    writeToFile(buffer, bytesRead);
                    
                    // 更新进度
                    notifyProgress(System.currentTimeMillis() - startTime);
                    
                } else if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
                    notifyError("无效操作错误");
                    break;
                } else if (bytesRead == AudioRecord.ERROR_BAD_VALUE) {
                    notifyError("参数错误");
                    break;
                } else {
                    notifyError("读取音频数据失败: " + bytesRead);
                    break;
                }
            }
        }
        
        private void writeToFile(byte[] data, int size) {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.write(data, 0, size);
                } catch (IOException e) {
                    Log.e(TAG, "写入文件失败: " + e.getMessage());
                }
            }
        }
    }
    
    private File createOutputFile(Context context, String extension) throws IOException {
        File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_MUSIC);
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
        String fileName = "recording_" + timeStamp + "." + extension;
        return new File(storageDir, fileName);
    }
    
    // 转换PCM为WAV文件
    public File convertToWav(Context context) {
        if (outputFile == null || !outputFile.exists()) {
            return null;
        }
        
        try {
            File wavFile = createOutputFile(context, "wav");
            convertPcmToWav(outputFile, wavFile, currentQuality.sampleRate, 
                           getChannelCount(currentQuality.channelConfig), 16);
            return wavFile;
        } catch (IOException e) {
            Log.e(TAG, "转换WAV失败: " + e.getMessage());
            return null;
        }
    }
    
    private void convertPcmToWav(File pcmFile, File wavFile, int sampleRate, 
                               int channels, int bitsPerSample) throws IOException {
        try (FileInputStream fis = new FileInputStream(pcmFile);
             FileOutputStream fos = new FileOutputStream(wavFile)) {
            
            // PCM数据长度
            long pcmDataLength = pcmFile.length();
            
            // WAV文件头
            byte[] header = createWavHeader(pcmDataLength, sampleRate, channels, bitsPerSample);
            fos.write(header);
            
            // 写入PCM数据
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }
        }
    }
    
    private byte[] createWavHeader(long pcmDataLength, int sampleRate, 
                                 int channels, int bitsPerSample) {
        byte[] header = new byte[44];
        long byteRate = sampleRate * channels * bitsPerSample / 8;
        long totalDataLength = pcmDataLength + 36;
        
        // RIFF header
        header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';
        writeInt(header, 4, (int) totalDataLength);
        header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E';
        
        // fmt chunk
        header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' ';
        writeInt(header, 16, 16); // PCM格式
        writeShort(header, 20, (short) 1); // 音频格式(PCM)
        writeShort(header, 22, (short) channels);
        writeInt(header, 24, sampleRate);
        writeInt(header, 28, (int) byteRate);
        writeShort(header, 32, (short) (channels * bitsPerSample / 8)); // 块对齐
        writeShort(header, 34, (short) bitsPerSample);
        
        // data chunk
        header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a';
        writeInt(header, 40, (int) pcmDataLength);
        
        return header;
    }
    
    private void writeShort(byte[] array, int offset, short value) {
        array[offset] = (byte) (value & 0xff);
        array[offset + 1] = (byte) ((value >> 8) & 0xff);
    }
    
    private void writeInt(byte[] array, int offset, int value) {
        array[offset] = (byte) (value & 0xff);
        array[offset + 1] = (byte) ((value >> 8) & 0xff);
        array[offset + 2] = (byte) ((value >> 16) & 0xff);
        array[offset + 3] = (byte) ((value >> 24) & 0xff);
    }
    
    private int getChannelCount(int channelConfig) {
        switch (channelConfig) {
            case AudioFormat.CHANNEL_IN_MONO:
                return 1;
            case AudioFormat.CHANNEL_IN_STEREO:
                return 2;
            default:
                return 1;
        }
    }
    
    private String getChannelConfigName(int channelConfig) {
        switch (channelConfig) {
            case AudioFormat.CHANNEL_IN_MONO: return "MONO";
            case AudioFormat.CHANNEL_IN_STEREO: return "STEREO";
            default: return "UNKNOWN";
        }
    }
    
    private String getAudioFormatName(int audioFormat) {
        switch (audioFormat) {
            case AudioFormat.ENCODING_PCM_8BIT: return "PCM_8BIT";
            case AudioFormat.ENCODING_PCM_16BIT: return "PCM_16BIT";
            case AudioFormat.ENCODING_PCM_FLOAT: return "PCM_FLOAT";
            default: return "UNKNOWN";
        }
    }
    
    // 回调通知方法
    private void notifyRecordingStarted() {
        if (callback != null) {
            new Handler(Looper.getMainLooper()).post(() -> 
                callback.onRecordingStarted());
        }
    }
    
    private void notifyRecordingStopped() {
        if (callback != null) {
            new Handler(Looper.getMainLooper()).post(() -> 
                callback.onRecordingStopped(outputFile));
        }
    }
    
    private void notifyError(String error) {
        if (callback != null) {
            new Handler(Looper.getMainLooper()).post(() -> 
                callback.onError(error));
        }
    }
    
    private void notifyAudioDataAvailable(byte[] data, int size) {
        if (callback != null) {
            // 注意:这里可能会频繁回调,需要确保回调处理效率
            byte[] copy = Arrays.copyOf(data, size);
            new Handler(Looper.getMainLooper()).post(() -> 
                callback.onAudioDataAvailable(copy, size));
        }
    }
    
    private void notifyProgress(long duration) {
        if (callback != null && duration % 1000 < 50) { // 约每秒通知一次
            new Handler(Looper.getMainLooper()).post(() -> 
                callback.onRecordingProgress(duration));
        }
    }
    
    public void setCallback(AudioRecorderCallback callback) {
        this.callback = callback;
    }
    
    public void release() {
        stopRecording();
        if (audioRecord != null) {
            audioRecord.release();
            audioRecord = null;
        }
    }
    
    public boolean isRecording() {
        return isRecording;
    }
    
    public AudioQuality getCurrentQuality() {
        return currentQuality;
    }
}

2. 实时音频分析示例

java 复制代码
public class AudioAnalyzer {
    private AdvancedAudioRecorder recorder;
    private boolean isAnalyzing = false;
    
    public interface AudioAnalysisCallback {
        void onVolumeChanged(double volume);
        void onFrequencyDetected(double frequency);
        void onSilenceDetected();
    }
    
    public void startAnalysis(Context context, AudioAnalysisCallback callback) {
        if (isAnalyzing) return;
        
        recorder = new AdvancedAudioRecorder(AdvancedAudioRecorder.AudioQuality.MEDIUM);
        recorder.setCallback(new AdvancedAudioRecorder.AudioRecorderCallback() {
            @Override
            public void onRecordingStarted() {}
            
            @Override
            public void onRecordingStopped(File outputFile) {}
            
            @Override
            public void onError(String error) {}
            
            @Override
            public void onAudioDataAvailable(byte[] data, int size) {
                analyzeAudioData(data, size, callback);
            }
            
            @Override
            public void onRecordingProgress(long duration) {}
        });
        
        if (recorder.initialize()) {
            recorder.startRecording(context);
            isAnalyzing = true;
        }
    }
    
    private void analyzeAudioData(byte[] data, int size, AudioAnalysisCallback callback) {
        // 计算音量(RMS)
        double volume = calculateVolume(data, size);
        if (callback != null) {
            callback.onVolumeChanged(volume);
        }
        
        // 检测静音
        if (volume < 0.01) { // 阈值可根据需要调整
            if (callback != null) {
                callback.onSilenceDetected();
            }
        }
        
        // 简单频率分析(FFT实现较复杂,这里用简化的过零率)
        double zeroCrossingRate = calculateZeroCrossingRate(data, size);
        // 可根据过零率估算基频(简化处理)
    }
    
    private double calculateVolume(byte[] data, int size) {
        long sum = 0;
        for (int i = 0; i < size - 1; i += 2) {
            // 将2个byte转换为16bit的short
            short audioSample = (short) ((data[i + 1] << 8) | (data[i] & 0xFF));
            sum += audioSample * audioSample;
        }
        
        double meanSquare = (double) sum / (size / 2);
        return Math.sqrt(meanSquare) / 32768.0; // 归一化到[0,1]
    }
    
    private double calculateZeroCrossingRate(byte[] data, int size) {
        int zeroCrossings = 0;
        short previousSample = 0;
        
        for (int i = 0; i < size - 1; i += 2) {
            short currentSample = (short) ((data[i + 1] << 8) | (data[i] & 0xFF));
            
            if ((previousSample >= 0 && currentSample < 0) || 
                (previousSample < 0 && currentSample >= 0)) {
                zeroCrossings++;
            }
            
            previousSample = currentSample;
        }
        
        return (double) zeroCrossings / (size / 2);
    }
    
    public void stopAnalysis() {
        if (recorder != null) {
            recorder.stopRecording();
            recorder.release();
        }
        isAnalyzing = false;
    }
}

3. 完整的录音 Activity

java 复制代码
public class AudioRecordingActivity extends AppCompatActivity {
    private AdvancedAudioRecorder audioRecorder;
    private AudioAnalyzer audioAnalyzer;
    
    private Button btnRecord;
    private TextView tvStatus, tvVolume;
    private ProgressBar volumeBar;
    private boolean isRecording = false;
    
    private static final int PERMISSION_REQUEST_CODE = 200;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_audio_recording);
        
        initViews();
        checkPermissions();
    }
    
    private void initViews() {
        btnRecord = findViewById(R.id.btnRecord);
        tvStatus = findViewById(R.id.tvStatus);
        tvVolume = findViewById(R.id.tvVolume);
        volumeBar = findViewById(R.id.volumeBar);
        
        btnRecord.setOnClickListener(v -> toggleRecording());
        
        // 初始化音频分析器(用于实时显示音量)
        audioAnalyzer = new AudioAnalyzer();
    }
    
    private void toggleRecording() {
        if (isRecording) {
            stopRecording();
        } else {
            startRecording();
        }
    }
    
    private void startRecording() {
        if (audioRecorder == null) {
            audioRecorder = new AdvancedAudioRecorder(AdvancedAudioRecorder.AudioQuality.HIGH);
            audioRecorder.setCallback(createRecorderCallback());
        }
        
        if (audioRecorder.initialize()) {
            audioRecorder.startRecording(this);
            isRecording = true;
            updateUI();
        }
    }
    
    private void stopRecording() {
        if (audioRecorder != null) {
            audioRecorder.stopRecording();
            
            // 转换为WAV格式
            File wavFile = audioRecorder.convertToWav(this);
            if (wavFile != null) {
                Toast.makeText(this, "音频已保存: " + wavFile.getName(), Toast.LENGTH_LONG).show();
            }
            
            audioRecorder.release();
            audioRecorder = null;
        }
        
        isRecording = false;
        updateUI();
    }
    
    private AdvancedAudioRecorder.AudioRecorderCallback createRecorderCallback() {
        return new AdvancedAudioRecorder.AudioRecorderCallback() {
            @Override
            public void onRecordingStarted() {
                runOnUiThread(() -> {
                    tvStatus.setText("录制中...");
                    tvStatus.setTextColor(Color.GREEN);
                });
            }
            
            @Override
            public void onRecordingStopped(File outputFile) {
                runOnUiThread(() -> {
                    tvStatus.setText("已停止");
                    tvStatus.setTextColor(Color.RED);
                });
            }
            
            @Override
            public void onError(String error) {
                runOnUiThread(() -> {
                    tvStatus.setText("错误: " + error);
                    tvStatus.setTextColor(Color.RED);
                    Toast.makeText(AudioRecordingActivity.this, error, Toast.LENGTH_LONG).show();
                });
            }
            
            @Override
            public void onAudioDataAvailable(byte[] data, int size) {
                // 实时分析音频数据
                analyzeRealTimeData(data, size);
            }
            
            @Override
            public void onRecordingProgress(long duration) {
                runOnUiThread(() -> {
                    long seconds = duration / 1000;
                    long minutes = seconds / 60;
                    seconds = seconds % 60;
                    tvStatus.setText(String.format("录制中 %02d:%02d", minutes, seconds));
                });
            }
        };
    }
    
    private void analyzeRealTimeData(byte[] data, int size) {
        // 计算实时音量
        double volume = calculateSimpleVolume(data, size);
        
        runOnUiThread(() -> {
            int progress = (int) (volume * 100);
            volumeBar.setProgress(Math.min(progress, 100));
            tvVolume.setText(String.format("音量: %d%%", progress));
        });
    }
    
    private double calculateSimpleVolume(byte[] data, int size) {
        long sum = 0;
        for (int i = 0; i < size - 1; i += 2) {
            short sample = (short) ((data[i + 1] << 8) | (data[i] & 0xFF));
            sum += Math.abs(sample);
        }
        return (double) sum / (size / 2) / 32768.0;
    }
    
    private void updateUI() {
        runOnUiThread(() -> {
            if (isRecording) {
                btnRecord.setText("停止录制");
                btnRecord.setBackgroundColor(Color.RED);
            } else {
                btnRecord.setText("开始录制");
                btnRecord.setBackgroundColor(Color.GREEN);
                volumeBar.setProgress(0);
                tvVolume.setText("音量: 0%");
            }
        });
    }
    
    private void checkPermissions() {
        String[] permissions = {
            Manifest.permission.RECORD_AUDIO,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        };
        
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) 
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE);
        }
    }
    
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == PERMISSION_REQUEST_CODE) {
            if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(this, "需要录音权限才能使用此功能", Toast.LENGTH_LONG).show();
                finish();
            }
        }
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        if (isRecording) {
            stopRecording();
        }
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (audioRecorder != null) {
            audioRecorder.release();
        }
    }
}

(六)对应布局文件

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="准备录制"
        android:textSize="24sp"
        android:textStyle="bold"
        android:gravity="center"
        android:layout_marginBottom="32dp" />

    <ProgressBar
        android:id="@+id/volumeBar"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="20dp"
        android:max="100"
        android:progress="0"
        android:layout_marginBottom="16dp" />

    <TextView
        android:id="@+id/tvVolume"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="音量: 0%"
        android:textSize="16sp"
        android:gravity="center"
        android:layout_marginBottom="32dp" />

    <Button
        android:id="@+id/btnRecord"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:text="开始录制"
        android:textSize="18sp"
        android:layout_gravity="center"
        android:background="@drawable/record_button_shape" />

</LinearLayout>

(七)注意事项

  1. 权限管理:确保在运行时请求录音权限

  2. 线程安全:AudioRecord 操作需要在非UI线程进行

  3. 缓冲区管理:合理设置缓冲区大小,避免数据丢失

  4. 性能优化:实时处理时注意算法效率

  5. 资源释放:及时释放 AudioRecord 资源

  6. 异常处理:妥善处理各种异常情况


(八)常见应用场景

  1. 语音录制:高质量音频录制

  2. 实时分析:音量检测、语音识别

  3. 音频处理:实时滤镜、特效处理

  4. 通信应用:VoIP、语音聊天

  5. 音乐应用:乐器调音、节拍器


六. AudioTrack

(一)介绍

AudioTrack 是 Android 中用于播放原始 PCM 音频数据的底层类,提供比 MediaPlayer 更底层的音频控制。


(二)核心特点

  • 原始 PCM 数据播放

  • 低延迟音频输出

  • 实时音频流处理

  • 灵活的音量控制


**(三)**使用

1. 基本配置

java 复制代码
public class SimpleAudioPlayer {
    private AudioTrack audioTrack;
    private int sampleRate = 44100;
    private int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
    private int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    private int bufferSize;
    
    public boolean initialize() {
        // 计算最小缓冲区大小
        bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
        
        if (bufferSize == AudioTrack.ERROR_BAD_VALUE) {
            return false;
        }
        
        // 创建 AudioTrack(流模式)
        audioTrack = new AudioTrack(
            AudioManager.STREAM_MUSIC,
            sampleRate,
            channelConfig,
            audioFormat,
            bufferSize,
            AudioTrack.MODE_STREAM
        );
        
        return audioTrack.getState() == AudioTrack.STATE_INITIALIZED;
    }
}

2. 播放音频数据

java 复制代码
public class AudioPlayer {
    private AudioTrack audioTrack;
    
    public void playPcmData(byte[] pcmData) {
        if (audioTrack == null || audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
            return;
        }
        
        // 开始播放
        audioTrack.play();
        
        // 写入音频数据
        int written = audioTrack.write(pcmData, 0, pcmData.length);
        
        if (written != pcmData.length) {
            Log.e("AudioTrack", "数据写入不完整");
        }
    }
    
    public void stop() {
        if (audioTrack != null) {
            audioTrack.stop();
            audioTrack.flush(); // 清空缓冲区
        }
    }
    
    public void release() {
        if (audioTrack != null) {
            audioTrack.release();
            audioTrack = null;
        }
    }
}

3. 流模式实时播放

java 复制代码
public class StreamAudioPlayer {
    private AudioTrack audioTrack;
    private Thread playbackThread;
    private boolean isPlaying = false;
    
    public void startStreaming() {
        audioTrack = new AudioTrack(
            AudioManager.STREAM_MUSIC,
            44100,
            AudioFormat.CHANNEL_OUT_STEREO,
            AudioFormat.ENCODING_PCM_16BIT,
            AudioTrack.getMinBufferSize(44100, AudioFormat.CHANNEL_OUT_STEREO, 
                                      AudioFormat.ENCODING_PCM_16BIT),
            AudioTrack.MODE_STREAM
        );
        
        audioTrack.play();
        isPlaying = true;
        
        playbackThread = new Thread(() -> {
            byte[] buffer = new byte[1024];
            while (isPlaying) {
                // 从网络或实时源获取音频数据
                int bytesRead = getAudioData(buffer);
                if (bytesRead > 0) {
                    audioTrack.write(buffer, 0, bytesRead);
                }
            }
        });
        playbackThread.start();
    }
    
    private int getAudioData(byte[] buffer) {
        // 实现音频数据获取逻辑
        // 例如:从网络流、实时录音等获取
        return 1024; // 示例
    }
}

4. 静态模式播放(适合短音效)

java 复制代码
public class StaticAudioPlayer {
    public void playShortSound(byte[] pcmData) {
        AudioTrack audioTrack = new AudioTrack(
            AudioManager.STREAM_MUSIC,
            44100,
            AudioFormat.CHANNEL_OUT_MONO,
            AudioFormat.ENCODING_PCM_16BIT,
            pcmData.length,
            AudioTrack.MODE_STATIC
        );
        
        // 一次性写入所有数据
        audioTrack.write(pcmData, 0, pcmData.length);
        audioTrack.play();
        
        // 播放完成后自动释放
        audioTrack.setNotificationMarkerPosition(pcmData.length / 2);
        audioTrack.setPlaybackPositionUpdateListener(new AudioTrack.OnPlaybackPositionUpdateListener() {
            @Override
            public void onMarkerReached(AudioTrack track) {
                track.release();
            }
            
            @Override
            public void onPeriodicNotification(AudioTrack track) {}
        });
    }
}

5. 音量控制

java 复制代码
public class VolumeControl {
    private AudioTrack audioTrack;
    
    public void setupVolume() {
        // 设置音量(0.0 - 1.0)
        float volume = 0.8f;
        int result = audioTrack.setVolume(volume);
        
        if (result != AudioTrack.SUCCESS) {
            Log.e("VolumeControl", "音量设置失败");
        }
        
        // 立体声音量分别控制
        audioTrack.setStereoVolume(0.7f, 1.0f); // 左声道70%,右声道100%
    }
    
    public void setPlaybackRate(int sampleRate) {
        // 动态调整播放速率
        audioTrack.setPlaybackRate(sampleRate);
    }
}

(四)使用示例

java 复制代码
// 播放 raw 资源中的 PCM 文件
public void playRawResource(Context context, int resId) {
    try {
        InputStream inputStream = context.getResources().openRawResource(resId);
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        
        byte[] data = new byte[1024];
        int bytesRead;
        while ((bytesRead = inputStream.read(data)) != -1) {
            buffer.write(data, 0, bytesRead);
        }
        
        byte[] pcmData = buffer.toByteArray();
        playPcmData(pcmData);
        
    } catch (IOException e) {
        e.printStackTrace();
    }
}

(五)关键参数说明

参数 说明 常用值
streamType 音频流类型 STREAM_MUSIC, STREAM_VOICE_CALL
sampleRate 采样率 8000, 16000, 44100 Hz
channelConfig 声道配置 CHANNEL_OUT_MONO, CHANNEL_OUT_STEREO
audioFormat 音频格式 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT
bufferSize 缓冲区大小 getMinBufferSize() 计算
mode 播放模式 MODE_STREAM, MODE_STATIC

七. MediaRouter

(一)介绍

MediaRouter 用于管理媒体输出路由,实现音频/视频在不同设备间的切换(如手机扬声器、蓝牙设备、Chromecast等)。


(二)核心功能

  • 设备发现:自动检测可用输出设备

  • 路由管理:在不同设备间切换媒体播放

  • 回调通知:监听设备连接状态变化


(三)使用

1. 添加依赖

java 复制代码
dependencies {
    implementation 'androidx.mediarouter:mediarouter:1.4.0'
    implementation 'com.google.android.gms:play-services-cast-framework:21.3.0'
}

2. 基本配置

java 复制代码
public class MediaRouterHelper {
    private MediaRouter mediaRouter;
    private MediaRouter.Callback callback;
    private MediaRouter.RouteInfo selectedRoute;
    
    public void initialize(Context context) {
        mediaRouter = MediaRouter.getInstance(context);
        
        // 创建回调
        callback = new MediaRouter.Callback() {
            @Override
            public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
                Log.d("MediaRouter", "设备添加: " + route.getName());
            }
            
            @Override
            public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
                selectedRoute = route;
                Log.d("MediaRouter", "设备选择: " + route.getName());
            }
            
            @Override
            public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) {
                Log.d("MediaRouter", "设备取消选择: " + route.getName());
            }
        };
        
        // 注册回调
        mediaRouter.addCallback(MediaRouter.ROUTE_TYPE_LIVE_AUDIO, callback, 
            MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
    }
}

3. 设备选择和播放控制

java 复制代码
public class MediaRouterPlayer {
    private MediaRouter mediaRouter;
    private MediaController mediaController;
    
    // 选择输出设备
    public void selectDevice(MediaRouter.RouteInfo route) {
        MediaRouter.RouteInfo currentRoute = mediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
        
        if (!route.equals(currentRoute)) {
            mediaRouter.selectRoute(route);
        }
    }
    
    // 获取所有可用设备
    public List<MediaRouter.RouteInfo> getAvailableDevices() {
        List<MediaRouter.RouteInfo> devices = new ArrayList<>();
        for (MediaRouter.RouteInfo route : mediaRouter.getRoutes()) {
            if (route.isEnabled()) {
                devices.add(route);
            }
        }
        return devices;
    }
    
    // 播放媒体到选定设备
    public void playMedia(Uri mediaUri) {
        MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
        
        if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
            // 远程播放(如Chromecast)
            playRemotely(route, mediaUri);
        } else {
            // 本地播放
            playLocally(mediaUri);
        }
    }
    
    private void playRemotely(MediaRouter.RouteInfo route, Uri mediaUri) {
        // 远程播放实现
        MediaControlIntent intent = new MediaControlIntent.Builder(mediaUri)
            .setTitle("媒体标题")
            .build();
        
        route.sendControlRequest(intent, null);
    }
}

4. 设备切换对话框

java 复制代码
public class DeviceSelectionHelper {
    
    // 显示设备选择对话框
    public void showDevicePicker(FragmentManager fragmentManager) {
        MediaRouteButton mediaRouteButton = new MediaRouteButton(context);
        MediaRouteDialogFactory factory = MediaRouteDialogFactory.getDefault();
        
        Dialog dialog = factory.onCreateDialog(MediaRouteDialogFactory.DIALOG_TYPE_DEFAULT);
        dialog.show();
    }
    
    // 或者在布局中添加MediaRouteButton
    // <androidx.mediarouter.app.MediaRouteButton
    //     android:id="@+id/media_route_button"
    //     android:layout_width="wrap_content"
    //     android:layout_height="wrap_content" />
}

5. Chromecast 集成

java 复制代码
public class ChromecastHelper {
    private CastContext castContext;
    private CastSession castSession;
    private SessionManagerListener<CastSession> sessionManagerListener;
    
    public void initializeCast(Context context) {
        castContext = CastContext.getSharedInstance(context);
        
        sessionManagerListener = new SessionManagerListener<CastSession>() {
            @Override
            public void onSessionStarted(CastSession session, String sessionId) {
                castSession = session;
                // Chromecast连接成功
            }
            
            @Override
            public void onSessionEnded(CastSession session, int error) {
                // Chromecast断开连接
                castSession = null;
            }
        };
        
        castContext.getSessionManager().addSessionManagerListener(sessionManagerListener);
    }
    
    public void castMedia(Uri mediaUri, String title) {
        if (castSession != null && castSession.isConnected()) {
            RemoteMediaClient client = castSession.getRemoteMediaClient();
            
            MediaInfo mediaInfo = new MediaInfo.Builder(mediaUri.toString())
                .setContentType("video/mp4")
                .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
                .setMetadata(new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
                    .putString(MediaMetadata.KEY_TITLE, title))
                .build();
            
            client.load(mediaInfo, true, 0);
        }
    }
}

6. 音频焦点管理

java 复制代码
public class AudioFocusHelper {
    private AudioManager audioManager;
    private AudioManager.OnAudioFocusChangeListener focusListener;
    
    public void setupAudioFocus(Context context) {
        audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        
        focusListener = new AudioManager.OnAudioFocusChangeListener() {
            @Override
            public void onAudioFocusChange(int focusChange) {
                switch (focusChange) {
                    case AudioManager.AUDIOFOCUS_GAIN:
                        // 重新获得焦点,恢复播放
                        break;
                    case AudioManager.AUDIOFOCUS_LOSS:
                        // 失去焦点,停止播放
                        break;
                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                        // 暂时失去焦点,暂停播放
                        break;
                }
            }
        };
        
        // 请求音频焦点
        int result = audioManager.requestAudioFocus(focusListener,
            AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
            
        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            // 可以开始播放
        }
    }
}

(四)完整使用示例

java 复制代码
public class MediaPlayerActivity extends AppCompatActivity {
    private MediaRouter mediaRouter;
    private MediaRouterHelper routerHelper;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_media_player);
        
        initializeMediaRouter();
        setupMediaControls();
    }
    
    private void initializeMediaRouter() {
        routerHelper = new MediaRouterHelper();
        routerHelper.initialize(this);
        
        // 设置MediaRouteButton
        MediaRouteButton routeButton = findViewById(R.id.media_route_button);
        MediaRouteSelector selector = new MediaRouteSelector.Builder()
            .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
            .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
            .build();
    }
    
    private void setupMediaControls() {
        Button playButton = findViewById(R.id.play_button);
        playButton.setOnClickListener(v -> {
            Uri mediaUri = Uri.parse("https://example.com/media.mp4");
            routerHelper.playMedia(mediaUri);
        });
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (routerHelper != null) {
            routerHelper.cleanup();
        }
    }
}

(五)关键类说明

类/接口 用途
MediaRouter 媒体路由管理器
RouteInfo 表示一个输出设备
MediaRouteProvider 扩展支持新设备类型
MediaControlIntent 远程播放控制

(六)常用路由类型

  • ROUTE_TYPE_LIVE_AUDIO- 音频输出

  • ROUTE_TYPE_LIVE_VIDEO- 视频输出

  • ROUTE_TYPE_USER- 用户自定义


(七)注意事项

  1. 权限检查:蓝牙等设备需要相应权限

  2. 生命周期管理:及时移除回调监听

  3. 错误处理:设备连接可能失败

  4. 用户体验:提供清晰的设备切换界面

相关推荐
正经教主4 小时前
【App开发】Mumu模拟器安装使用与Android Studio连接指南
android·ide·android studio
Larry_zhang双栖4 小时前
Flutter Android Kotlin 插件编译错误完整解决方案
android·flutter·kotlin
wuwu_q5 小时前
彻底讲清楚 Kotlin 的 when 表达式
android·开发语言·kotlin
木易 士心5 小时前
Android 开发核心技术深度解析
android·开发语言·python
QING6186 小时前
Jetpack Compose 条件布局与 Layout 内在测量详解
android·kotlin·android jetpack
一念杂记6 小时前
没网太崩溃!手机电脑网络共享,简单几步搞定网络共享,再也不用为没网担忧~
android·windows
我是好小孩7 小时前
【Android】Binder 原理初探:理解 Android 进程通信机制
android·gitee·binder
-指短琴长-7 小时前
ProtoBuf速成【基于C++讲解】
android·java·c++
下位子7 小时前
『OpenGL学习滤镜相机』- Day4: 纹理贴图基础
android·opengl