Khung đa phương tiện của Android bao gồm tính năng hỗ trợ ghi lại và mã hoá nhiều định dạng âm thanh và video phổ biến. Bạn có thể sử dụng các API MediaRecorder
nếu phần cứng thiết bị hỗ trợ.
Tài liệu này cho bạn biết cách sử dụng MediaRecorder
để ghi một ứng dụng thu âm thanh từ micrô của thiết bị, lưu âm thanh và phát lại (bằng MediaPlayer
). Để quay video, bạn cần sử dụng máy ảnh của thiết bị cùng với MediaRecorder
. Tính năng này được mô tả trong hướng dẫn về Máy ảnh.
Lưu ý: Trình mô phỏng Android không thể ghi âm. Hãy nhớ kiểm thử mã của bạn trên một thiết bị thực có khả năng ghi.
Yêu cầu cấp quyền ghi âm
Để có thể ghi, ứng dụng của bạn phải cho người dùng biết rằng ứng dụng sẽ truy cập vào đầu vào âm thanh của thiết bị. Bạn phải đưa thẻ quyền này vào tệp kê khai của ứng dụng:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
RECORD_AUDIO
được coi là một quyền "nguy hiểm" vì có thể gây rủi ro cho quyền riêng tư của người dùng. Kể từ Android 6.0 (API cấp 23), một ứng dụng dùng quyền nguy hiểm phải yêu cầu người dùng phê duyệt trong thời gian chạy. Sau khi người dùng cấp quyền, ứng dụng sẽ ghi nhớ và không hỏi lại. Mã mẫu bên dưới cho biết cách
triển khai hành vi này bằng
ActivityCompat.requestPermissions()
.
Tạo và chạy MediaRecorder
Khởi động một thực thể mới của MediaRecorder
bằng các lệnh gọi sau:
- Đặt nguồn âm thanh bằng
setAudioSource()
. Có thể bạn sẽ dùngMIC
.Lưu ý: Hầu hết các nguồn âm thanh (bao gồm cả
DEFAULT
) đều áp dụng quá trình xử lý cho tín hiệu âm thanh. Để ghi lại âm thanh thô, hãy chọnUNPROCESSED
. Một số thiết bị không hỗ trợ dữ liệu đầu vào chưa được xử lý. Trước tiên, hãy gọiAudioManager.getProperty(AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED)
để xác minh rằng bạn có thể dùng dịch vụ này. Nếu không, hãy thử dùngVOICE_RECOGNITION
. Tính năng này không sử dụng AGC hoặc tính năng khử tiếng ồn. Bạn có thể sử dụngUNPROCESSED
làm nguồn âm thanh ngay cả khi thuộc tính này không được hỗ trợ. Tuy nhiên, không có gì đảm bảo rằng tín hiệu sẽ chưa được xử lý trong trường hợp đó. - Đặt định dạng tệp đầu ra bằng
setOutputFormat()
. Lưu ý rằng kể từ Android 8.0 (API cấp 26)MediaRecorder
sẽ hỗ trợ định dạng MPEG2_TS. Định dạng này rất hữu ích trong việc truyền trực tuyến:Kotlin
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_2_TS)
Java
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_2_TS);
- Đặt tên tệp đầu ra bằng cách sử dụng
setOutputFile()
. Bạn phải chỉ định một chỉ số mô tả tệp đại diện cho một tệp thực tế. - Đặt bộ mã hoá âm thanh bằng
setAudioEncoder()
. - Hoàn tất quá trình khởi chạy bằng cách gọi
prepare()
.
Khởi động và dừng trình ghi bằng cách gọi lần lượt start()
và stop()
.
Khi bạn hoàn tất thực thể MediaRecorder
, hãy giải phóng tài nguyên của thực thể đó càng sớm càng tốt bằng cách gọi release()
.
Lưu ý: Trên các thiết bị chạy Android 9 (API cấp 28) trở lên, các ứng dụng chạy ở chế độ nền sẽ không thể truy cập vào micrô. Do đó, ứng dụng của bạn chỉ nên ghi âm khi ở nền trước hoặc khi bạn đưa một thực thể của MediaRecorder
vào dịch vụ trên nền trước.
Sử dụng MediaMuxer để ghi lại nhiều kênh
Kể từ Android 8.0 (API cấp 26), bạn có thể dùng MediaMuxer
để ghi lại đồng thời nhiều luồng âm thanh và video. Trong các phiên bản Android cũ hơn, bạn chỉ có thể ghi mỗi lần một bản âm thanh và/hoặc một bản video.
Sử dụng phương thức addTrack()
để kết hợp nhiều bản nhạc với nhau.
Bạn cũng có thể thêm một hoặc nhiều bản siêu dữ liệu có thông tin tuỳ chỉnh cho từng khung hình, nhưng chỉ cho vùng chứa MP4. Ứng dụng của bạn xác định định dạng và nội dung của siêu dữ liệu.
Thêm siêu dữ liệu
Siêu dữ liệu có thể hữu ích cho việc xử lý ngoại tuyến. Ví dụ: dữ liệu được thu thập từ cảm biến con quay hồi chuyển có thể được dùng để ổn định video.
Khi bạn thêm một bản nhạc siêu dữ liệu, định dạng MIME của bản nhạc đó phải bắt đầu bằng tiền tố application/
. Việc ghi siêu dữ liệu cũng giống như việc ghi dữ liệu âm thanh hoặc video, ngoại trừ việc dữ liệu không đến từ MediaCodec
. Thay vào đó, ứng dụng sẽ chuyển một ByteBuffer
có dấu thời gian liên kết đến phương thức writeSampleData()
.
Dấu thời gian phải trùng với thời điểm của bản âm thanh và video.
Tệp MP4 được tạo sẽ sử dụng TextMetaDataSampleEntry
được xác định trong mục 12.3.3.2 của thông số kỹ thuật ISO BMFF để báo hiệu định dạng mime của siêu dữ liệu. Khi bạn sử dụng MediaExtractor
để trích xuất tệp chứa các bản nhạc siêu dữ liệu, định dạng mime của siêu dữ liệu sẽ xuất hiện dưới dạng một thực thể của MediaFormat
.
Mã mẫu
Mẫu MediaRecorder minh hoạ cách quay video bằng MediaRecorder và API Camera.
Hoạt động mẫu dưới đây cho biết cách sử dụng MediaRecorder
để ghi tệp âm thanh. Tệp này cũng sử dụng MediaPlayer
để phát lại âm thanh.
Kotlin
package com.android.audiorecordtest import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.media.MediaPlayer import android.media.MediaRecorder import android.os.Bundle import android.support.v4.app.ActivityCompat import android.support.v7.app.AppCompatActivity import android.util.Log import android.view.View.OnClickListener import android.view.ViewGroup import android.widget.Button import android.widget.LinearLayout import java.io.IOException private const val LOG_TAG = "AudioRecordTest" private const val REQUEST_RECORD_AUDIO_PERMISSION = 200 class AudioRecordTest : AppCompatActivity() { private var fileName: String = "" private var recordButton: RecordButton? = null private var recorder: MediaRecorder? = null private var playButton: PlayButton? = null private var player: MediaPlayer? = null // Requesting permission to RECORD_AUDIO private var permissionToRecordAccepted = false private var permissions: Array<String> = arrayOf(Manifest.permission.RECORD_AUDIO) override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) permissionToRecordAccepted = if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) { grantResults[0] == PackageManager.PERMISSION_GRANTED } else { false } if (!permissionToRecordAccepted) finish() } private fun onRecord(start: Boolean) = if (start) { startRecording() } else { stopRecording() } private fun onPlay(start: Boolean) = if (start) { startPlaying() } else { stopPlaying() } private fun startPlaying() { player = MediaPlayer().apply { try { setDataSource(fileName) prepare() start() } catch (e: IOException) { Log.e(LOG_TAG, "prepare() failed") } } } private fun stopPlaying() { player?.release() player = null } private fun startRecording() { recorder = MediaRecorder().apply { setAudioSource(MediaRecorder.AudioSource.MIC) setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) setOutputFile(fileName) setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) try { prepare() } catch (e: IOException) { Log.e(LOG_TAG, "prepare() failed") } start() } } private fun stopRecording() { recorder?.apply { stop() release() } recorder = null } internal inner class RecordButton(ctx: Context) : Button(ctx) { var mStartRecording = true var clicker: OnClickListener = OnClickListener { onRecord(mStartRecording) text = when (mStartRecording) { true -> "Stop recording" false -> "Start recording" } mStartRecording = !mStartRecording } init { text = "Start recording" setOnClickListener(clicker) } } internal inner class PlayButton(ctx: Context) : Button(ctx) { var mStartPlaying = true var clicker: OnClickListener = OnClickListener { onPlay(mStartPlaying) text = when (mStartPlaying) { true -> "Stop playing" false -> "Start playing" } mStartPlaying = !mStartPlaying } init { text = "Start playing" setOnClickListener(clicker) } } override fun onCreate(icicle: Bundle?) { super.onCreate(icicle) // Record to the external cache directory for visibility fileName = "${externalCacheDir.absolutePath}/audiorecordtest.3gp" ActivityCompat.requestPermissions(this, permissions, REQUEST_RECORD_AUDIO_PERMISSION) recordButton = RecordButton(this) playButton = PlayButton(this) val ll = LinearLayout(this).apply { addView(recordButton, LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0f)) addView(playButton, LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0f)) } setContentView(ll) } override fun onStop() { super.onStop() recorder?.release() recorder = null player?.release() player = null } }
Java
package com.android.audiorecordtest; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.media.MediaPlayer; import android.media.MediaRecorder; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; import java.io.IOException; public class AudioRecordTest extends AppCompatActivity { private static final String LOG_TAG = "AudioRecordTest"; private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200; private static String fileName = null; private RecordButton recordButton = null; private MediaRecorder recorder = null; private PlayButton playButton = null; private MediaPlayer player = null; // Requesting permission to RECORD_AUDIO private boolean permissionToRecordAccepted = false; private String [] permissions = {Manifest.permission.RECORD_AUDIO}; @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode){ case REQUEST_RECORD_AUDIO_PERMISSION: permissionToRecordAccepted = grantResults[0] == PackageManager.PERMISSION_GRANTED; break; } if (!permissionToRecordAccepted ) finish(); } private void onRecord(boolean start) { if (start) { startRecording(); } else { stopRecording(); } } private void onPlay(boolean start) { if (start) { startPlaying(); } else { stopPlaying(); } } private void startPlaying() { player = new MediaPlayer(); try { player.setDataSource(fileName); player.prepare(); player.start(); } catch (IOException e) { Log.e(LOG_TAG, "prepare() failed"); } } private void stopPlaying() { player.release(); player = null; } private void startRecording() { recorder = new MediaRecorder(); recorder.setAudioSource(MediaRecorder.AudioSource.MIC); recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); recorder.setOutputFile(fileName); recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); try { recorder.prepare(); } catch (IOException e) { Log.e(LOG_TAG, "prepare() failed"); } recorder.start(); } private void stopRecording() { recorder.stop(); recorder.release(); recorder = null; } class RecordButton extends Button { boolean mStartRecording = true; OnClickListener clicker = new OnClickListener() { public void onClick(View v) { onRecord(mStartRecording); if (mStartRecording) { setText("Stop recording"); } else { setText("Start recording"); } mStartRecording = !mStartRecording; } }; public RecordButton(Context ctx) { super(ctx); setText("Start recording"); setOnClickListener(clicker); } } class PlayButton extends Button { boolean mStartPlaying = true; OnClickListener clicker = new OnClickListener() { public void onClick(View v) { onPlay(mStartPlaying); if (mStartPlaying) { setText("Stop playing"); } else { setText("Start playing"); } mStartPlaying = !mStartPlaying; } }; public PlayButton(Context ctx) { super(ctx); setText("Start playing"); setOnClickListener(clicker); } } @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); // Record to the external cache directory for visibility fileName = getExternalCacheDir().getAbsolutePath(); fileName += "/audiorecordtest.3gp"; ActivityCompat.requestPermissions(this, permissions, REQUEST_RECORD_AUDIO_PERMISSION); LinearLayout ll = new LinearLayout(this); recordButton = new RecordButton(this); ll.addView(recordButton, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0)); playButton = new PlayButton(this); ll.addView(playButton, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0)); setContentView(ll); } @Override public void onStop() { super.onStop(); if (recorder != null) { recorder.release(); recorder = null; } if (player != null) { player.release(); player = null; } } }
Tìm hiểu thêm
Những trang này đề cập đến những chủ đề liên quan đến việc ghi lại, lưu trữ và phát âm thanh cũng như video.