Android音訊實時傳輸與播放(三):AMR硬編碼與硬解碼
原文連結:http://blog.csdn.net/zgyulongfei/article/details/7753163
在Android中我所知道的音訊編解碼有兩種方式:
(一)使用AudioRecord採集音訊,用這種方式採集的是未經壓縮的音訊流;用AudioTrack播放實時音訊流。用這兩個類的話,如果需要對音訊進行編解碼,就需要自己移植編解碼庫了,比如可以移植ilbc,speex等開源編解碼庫。
ilbc的編解碼實現可以檢視這個專欄:http://blog.csdn.net/column/details/media.html
(二)使用MediaRecorder獲取編碼後的AMR音訊,但由於MediaRecorder的特點,只能將流儲存到檔案中,但通過其他方式是可以獲取到實時音訊流的,這篇文章將介紹用LocalSocket的方法來實現;使用MediaPlayer來播放AMR音訊流,但同樣MediaPlayer也只能播放檔案流,因此我用快取的方式來播放音訊。
以上兩種方式各有利弊,使用方法(一)需移植編解碼庫,但可以播放實時音訊流;使用方法(二)直接硬編硬解碼效率高,但是需要對檔案進行操作。
PS:這篇文章只是給大家一個參考,僅供學習之用,如果真正用到專案中還有很多地方需要優化。
我強烈推薦播放音訊時候用方法(一),方法(二)雖然能夠實現功能,但是實現方式不太好。
接下來看程式碼:
編碼器:
- package cn.edu.xmu.zgy.audio.encoder;
- import java.io.DataInputStream;
- import java.io.IOException;
- import java.net.DatagramPacket;
- import java.net.DatagramSocket;
- import java.net.InetAddress;
- import cn.edu.xmu.zgy.config.CommonConfig;
- import android.app.Activity;
- import android.media.MediaRecorder;
- import android.net.LocalServerSocket;
- import android.net.LocalSocket;
- import android.net.LocalSocketAddress;
- import android.util.Log;
- import android.widget.Toast;
- //blog.csdn.net/zgyulongfei
- //Email: zgyulongfei@gmail.com
- public class AmrAudioEncoder {
- private static final String TAG = "ArmAudioEncoder";
- private static AmrAudioEncoder amrAudioEncoder = null;
- private Activity activity;
- private MediaRecorder audioRecorder;
- private boolean isAudioRecording;
- private LocalServerSocket lss;
- private LocalSocket sender, receiver;
- private AmrAudioEncoder() {
- }
- public static AmrAudioEncoder getArmAudioEncoderInstance() {
- if (amrAudioEncoder == null) {
- synchronized (AmrAudioEncoder.class) {
- if (amrAudioEncoder == null) {
- amrAudioEncoder = new AmrAudioEncoder();
- }
- }
- }
- return amrAudioEncoder;
- }
- public void initArmAudioEncoder(Activity activity) {
- this.activity = activity;
- isAudioRecording = false;
- }
- public void start() {
- if (activity == null) {
- showToastText("音訊編碼器未初始化,請先執行init方法");
- return;
- }
- if (isAudioRecording) {
- showToastText("音訊已經開始編碼,無需再次編碼");
- return;
- }
- if (!initLocalSocket()) {
- showToastText("本地服務開啟失敗");
- releaseAll();
- return;
- }
- if (!initAudioRecorder()) {
- showToastText("音訊編碼器初始化失敗");
- releaseAll();
- return;
- }
- this.isAudioRecording = true;
- startAudioRecording();
- }
- private boolean initLocalSocket() {
- boolean ret = true;
- try {
- releaseLocalSocket();
- String serverName = "armAudioServer";
- final int bufSize = 1024;
- lss = new LocalServerSocket(serverName);
- receiver = new LocalSocket();
- receiver.connect(new LocalSocketAddress(serverName));
- receiver.setReceiveBufferSize(bufSize);
- receiver.setSendBufferSize(bufSize);
- sender = lss.accept();
- sender.setReceiveBufferSize(bufSize);
- sender.setSendBufferSize(bufSize);
- } catch (IOException e) {
- ret = false;
- }
- return ret;
- }
- private boolean initAudioRecorder() {
- if (audioRecorder != null) {
- audioRecorder.reset();
- audioRecorder.release();
- }
- audioRecorder = new MediaRecorder();
- audioRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
- audioRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);
- final int mono = 1;
- audioRecorder.setAudioChannels(mono);
- audioRecorder.setAudioSamplingRate(8000);
- audioRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
- audioRecorder.setOutputFile(sender.getFileDescriptor());
- boolean ret = true;
- try {
- audioRecorder.prepare();
- audioRecorder.start();
- } catch (Exception e) {
- releaseMediaRecorder();
- showToastText("手機不支援錄音此功能");
- ret = false;
- }
- return ret;
- }
- private void startAudioRecording() {
- new Thread(new AudioCaptureAndSendThread()).start();
- }
- public void stop() {
- if (isAudioRecording) {
- isAudioRecording = false;
- }
- releaseAll();
- }
- private void releaseAll() {
- releaseMediaRecorder();
- releaseLocalSocket();
- amrAudioEncoder = null;
- }
- private void releaseMediaRecorder() {
- try {
- if (audioRecorder == null) {
- return;
- }
- if (isAudioRecording) {
- audioRecorder.stop();
- isAudioRecording = false;
- }
- audioRecorder.reset();
- audioRecorder.release();
- audioRecorder = null;
- } catch (Exception err) {
- Log.d(TAG, err.toString());
- }
- }
- private void releaseLocalSocket() {
- try {
- if (sender != null) {
- sender.close();
- }
- if (receiver != null) {
- receiver.close();
- }
- if (lss != null) {
- lss.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- sender = null;
- receiver = null;
- lss = null;
- }
- private boolean isAudioRecording() {
- return isAudioRecording;
- }
- private void showToastText(String msg) {
- Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show();
- }
- private class AudioCaptureAndSendThread implements Runnable {
- public void run() {
- try {
- sendAmrAudio();
- } catch (Exception e) {
- Log.e(TAG, "sendAmrAudio() 出錯");
- }
- }
- private void sendAmrAudio() throws Exception {
- DatagramSocket udpSocket = new DatagramSocket();
- DataInputStream dataInput = new DataInputStream(receiver.getInputStream());
- skipAmrHead(dataInput);
- final int SEND_FRAME_COUNT_ONE_TIME = 10;// 每次傳送10幀的資料,1幀大約32B
- // AMR格式見部落格:http://blog.csdn.net/dinggo/article/details/1966444
- final int BLOCK_SIZE[] = { 12, 13, 15, 17, 19, 20, 26, 31, 5, 0, 0, 0, 0, 0, 0, 0 };
- byte[] sendBuffer = new byte[1024];
- while (isAudioRecording()) {
- int offset = 0;
- for (int index = 0; index < SEND_FRAME_COUNT_ONE_TIME; ++index) {
- if (!isAudioRecording()) {
- break;
- }
- dataInput.read(sendBuffer, offset, 1);
- int blockIndex = (int) (sendBuffer[offset] >> 3) & 0x0F;
- int frameLength = BLOCK_SIZE[blockIndex];
- readSomeData(sendBuffer, offset + 1, frameLength, dataInput);
- offset += frameLength + 1;
- }
- udpSend(udpSocket, sendBuffer, offset);
- }
- udpSocket.close();
- dataInput.close();
- releaseAll();
- }
- private void skipAmrHead(DataInputStream dataInput) {
- final byte[] AMR_HEAD = new byte[] { 0x23, 0x21, 0x41, 0x4D, 0x52, 0x0A };
- int result = -1;
- int state = 0;
- try {
- while (-1 != (result = dataInput.readByte())) {
- if (AMR_HEAD[0] == result) {
- state = (0 == state) ? 1 : 0;
- } else if (AMR_HEAD[1] == result) {
- state = (1 == state) ? 2 : 0;
- } else if (AMR_HEAD[2] == result) {
- state = (2 == state) ? 3 : 0;
- } else if (AMR_HEAD[3] == result) {
- state = (3 == state) ? 4 : 0;
- } else if (AMR_HEAD[4] == result) {
- state = (4 == state) ? 5 : 0;
- } else if (AMR_HEAD[5] == result) {
- state = (5 == state) ? 6 : 0;
- }
- if (6 == state) {
- break;
- }
- }
- } catch (Exception e) {
- Log.e(TAG, "read mdat error...");
- }
- }
- private void readSomeData(byte[] buffer, int offset, int length, DataInputStream dataInput) {
- int numOfRead = -1;
- while (true) {
- try {
- numOfRead = dataInput.read(buffer, offset, length);
- if (numOfRead == -1) {
- Log.d(TAG, "amr...no data get wait for data coming.....");
- Thread.sleep(100);
- } else {
- offset += numOfRead;
- length -= numOfRead;
- if (length <= 0) {
- break;
- }
- }
- } catch (Exception e) {
- Log.e(TAG, "amr..error readSomeData");
- break;
- }
- }
- }
- private void udpSend(DatagramSocket udpSocket, byte[] buffer, int sendLength) {
- try {
- InetAddress ip = InetAddress.getByName(CommonConfig.SERVER_IP_ADDRESS.trim());
- int port = CommonConfig.AUDIO_SERVER_UP_PORT;
- byte[] sendBuffer = new byte[sendLength];
- System.arraycopy(buffer, 0, sendBuffer, 0, sendLength);
- DatagramPacket packet = new DatagramPacket(sendBuffer, sendLength);
- packet.setAddress(ip);
- packet.setPort(port);
- udpSocket.send(packet);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
關於編碼器:前面提到了,MediaRecorder的硬編碼的方式只能將碼流儲存到檔案中,這裡用了LocalSocket的方式將流儲存到記憶體中,然後從緩衝中讀取碼流。由於儲存的格式RAW_AMR格式的,因此需要對讀取到的資料進行解析,從而獲得真正的音訊流。想了解AMR音訊碼流格式的,可以檢視程式碼中附上的網頁連結。由於壓縮過的碼流很小,因此我在實現的時候,組合了int SEND_FRAME_COUNT_ONE_TIME = 10幀的碼流後才往外傳送,這樣的方式造成的延遲會加重,大家可以根據自己的需要進行修改。造成延遲的另一因素是LocalSocket緩衝的大小,在這裡我設定的大小是final int bufSize = 1024;程式碼寫的很清楚詳細,有疑問的可以提出。
播放器:
- package cn.edu.xmu.zgy.audio.player;
- import java.io.BufferedInputStream;
- import java.io.BufferedOutputStream;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.net.InetAddress;
- import java.net.Socket;
- import cn.edu.xmu.zgy.config.CommonConfig;
- import android.app.Activity;
- import android.media.MediaPlayer;
- import android.os.Handler;
- import android.util.Log;
- //blog.csdn.net/zgyulongfei
- //Email: zgyulongfei@gmail.com
- public class AmrAudioPlayer {
- private static final String TAG = "AmrAudioPlayer";
- private static AmrAudioPlayer playerInstance = null;
- private long alreadyReadByteCount = 0;
- private MediaPlayer audioPlayer;
- private Handler handler = new Handler();
- private final String cacheFileName = "audioCacheFile";
- private File cacheFile;
- private int cacheFileCount = 0;
- // 用來記錄是否已經從cacheFile中複製資料到另一個cache中
- private boolean hasMovedTheCacheFlag;
- private boolean isPlaying;
- private Activity activity;
- private boolean isChaingCacheToAnother;
- private AmrAudioPlayer() {
- }
- public static AmrAudioPlayer getAmrAudioPlayerInstance() {
- if (playerInstance == null) {
- synchronized (AmrAudioPlayer.class) {
- if (playerInstance == null) {
- playerInstance = new AmrAudioPlayer();
- }
- }
- }
- return playerInstance;
- }
- public void initAmrAudioPlayer(Activity activity) {
- this.activity = activity;
- deleteExistCacheFile();
- initCacheFile();
- }
- private void deleteExistCacheFile() {
- File cacheDir = activity.getCacheDir();
- File[] needDeleteCacheFiles = cacheDir.listFiles();
- for (int index = 0; index < needDeleteCacheFiles.length; ++index) {
- File cache = needDeleteCacheFiles[index];
- if (cache.isFile()) {
- if (cache.getName().contains(cacheFileName.trim())) {
- Log.e(TAG, "delete cache file: " + cache.getName());
- cache.delete();
- }
- }
- }
- needDeleteCacheFiles = null;
- }
- private void initCacheFile() {
- cacheFile = null;
- cacheFile = new File(activity.getCacheDir(), cacheFileName);
- }
- public void start() {
- isPlaying = true;
- isChaingCacheToAnother = false;
- setHasMovedTheCacheToAnotherCache(false);
- new Thread(new NetAudioPlayerThread()).start();
- }
- public void stop() {
- isPlaying = false;
- isChaingCacheToAnother = false;
- setHasMovedTheCacheToAnotherCache(false);
- releaseAudioPlayer();
- deleteExistCacheFile();
- cacheFile = null;
- handler = null;
- }
- private void releaseAudioPlayer() {
- playerInstance = null;
- if (audioPlayer != null) {
- try {
- if (audioPlayer.isPlaying()) {
- audioPlayer.pause();
- }
- audioPlayer.release();
- audioPlayer = null;
- } catch (Exception e) {
- }
- }
- }
- private boolean hasMovedTheCacheToAnotherCache() {
- return hasMovedTheCacheFlag;
- }
- private void setHasMovedTheCacheToAnotherCache(boolean result) {
- hasMovedTheCacheFlag = result;
- }
- private class NetAudioPlayerThread implements Runnable {
- // 從接受資料開始計算,當快取大於INIT_BUFFER_SIZE時候開始播放
- private final int INIT_AUDIO_BUFFER = 2 * 1024;
- // 剩1秒的時候播放新的快取的音樂
- private final int CHANGE_CACHE_TIME = 1000;
- public void run() {
- try {
- Socket socket = createSocketConnectToServer();
- receiveNetAudioThenPlay(socket);
- } catch (Exception e) {
- Log.e(TAG, e.getMessage() + "從服務端接受音訊失敗。。。");
- }
- }
- private Socket createSocketConnectToServer() throws Exception {
- String hostName = CommonConfig.SERVER_IP_ADDRESS;
- InetAddress ipAddress = InetAddress.getByName(hostName);
- int port = CommonConfig.AUDIO_SERVER_DOWN_PORT;
- Socket socket = new Socket(ipAddress, port);
- return socket;
- }
- private void receiveNetAudioThenPlay(Socket socket) throws Exception {
- InputStream inputStream = socket.getInputStream();
- FileOutputStream outputStream = new FileOutputStream(cacheFile);
- final int BUFFER_SIZE = 100 * 1024;// 100kb buffer size
- byte[] buffer = new byte[BUFFER_SIZE];
- // 收集了10*350b了之後才開始更換快取
- int testTime = 10;
- try {
- alreadyReadByteCount = 0;
- while (isPlaying) {
- int numOfRead = inputStream.read(buffer);
- if (numOfRead <= 0) {
- break;
- }
- alreadyReadByteCount += numOfRead;
- outputStream.write(buffer, 0, numOfRead);
- outputStream.flush();
- try {
- if (testTime++ >= 10) {
- Log.e(TAG, "cacheFile=" + cacheFile.length());
- testWhetherToChangeCache();
- testTime = 0;
- }
- } catch (Exception e) {
- // TODO: handle exception
- }
- // 如果複製了接收網路流的cache,則執行此操作
- if (hasMovedTheCacheToAnotherCache() && !isChaingCacheToAnother) {
- if (outputStream != null) {
- outputStream.close();
- outputStream = null;
- }
- // 將接收網路流的cache刪除,然後重0開始儲存
- // initCacheFile();
- outputStream = new FileOutputStream(cacheFile);
- setHasMovedTheCacheToAnotherCache(false);
- alreadyReadByteCount = 0;
- }
- }
- } catch (Exception e) {
- errorOperator();
- e.printStackTrace();
- Log.e(TAG, "socket disconnect...:" + e.getMessage());
- throw new Exception("socket disconnect....");
- } finally {
- buffer = null;
- if (socket != null) {
- socket.close();
- }
- if (inputStream != null) {
- inputStream.close();
- inputStream = null;
- }
- if (outputStream != null) {
- outputStream.close();
- outputStream = null;
- }
- stop();
- }
- }
- private void testWhetherToChangeCache() throws Exception {
- if (audioPlayer == null) {
- firstTimeStartPlayer();
- } else {
- changeAnotherCacheWhenEndOfCurrentCache();
- }
- }
- private void firstTimeStartPlayer() throws Exception {
- // 當快取已經大於INIT_AUDIO_BUFFER則開始播放
- if (alreadyReadByteCount >= INIT_AUDIO_BUFFER) {
- Runnable r = new Runnable() {
- public void run() {
- try {
- File firstCacheFile = createFirstCacheFile();
- // 設定已經從cache中複製資料,然後會刪除這個cache
- setHasMovedTheCacheToAnotherCache(true);
- audioPlayer = createAudioPlayer(firstCacheFile);
- audioPlayer.start();
- } catch (Exception e) {
- Log.e(TAG, e.getMessage() + " :in firstTimeStartPlayer() fun");
- } finally {
- }
- }
- };
- handler.post(r);
- }
- }
- private File createFirstCacheFile() throws Exception {
- String firstCacheFileName = cacheFileName + (cacheFileCount++);
- File firstCacheFile = new File(activity.getCacheDir(), firstCacheFileName);
- // 為什麼不直接播放cacheFile,而要複製cacheFile到一個新的cache,然後播放此新的cache?
- // 是為了防止潛在的讀/寫錯誤,可能在寫入cacheFile的時候,
- // MediaPlayer正試圖讀資料, 這樣可以防止死鎖的發生。
- moveFile(cacheFile, firstCacheFile);
- return firstCacheFile;
- }
- private void moveFile(File oldFile, File newFile) throws IOException {
- if (!oldFile.exists()) {
- throw new IOException("oldFile is not exists. in moveFile() fun");
- }
- if (oldFile.length() <= 0) {
- throw new IOException("oldFile size = 0. in moveFile() fun");
- }
- BufferedInputStream reader = new BufferedInputStream(new FileInputStream(oldFile));
- BufferedOutputStream writer = new BufferedOutputStream(new FileOutputStream(newFile,
- false));
- final byte[] AMR_HEAD = new byte[] { 0x23, 0x21, 0x41, 0x4D, 0x52, 0x0A };
- writer.write(AMR_HEAD, 0, AMR_HEAD.length);
- writer.flush();
- try {
- byte[] buffer = new byte[1024];
- int numOfRead = 0;
- Log.d(TAG, "POS...newFile.length=" + newFile.length() + " old=" + oldFile.length());
- while ((numOfRead = reader.read(buffer, 0, buffer.length)) != -1) {
- writer.write(buffer, 0, numOfRead);
- writer.flush();
- }
- Log.d(TAG, "POS..AFTER...newFile.length=" + newFile.length());
- } catch (IOException e) {
- Log.e(TAG, "moveFile error.. in moveFile() fun." + e.getMessage());
- throw new IOException("moveFile error.. in moveFile() fun.");
- } finally {
- if (reader != null) {
- reader.close();
- reader = null;
- }
- if (writer != null) {
- writer.close();
- writer = null;
- }
- }
- }
- private MediaPlayer createAudioPlayer(File audioFile) throws IOException {
- MediaPlayer mPlayer = new MediaPlayer();
- // It appears that for security/permission reasons, it is better to
- // pass
- // a FileDescriptor rather than a direct path to the File.
- // Also I have seen errors such as "PVMFErrNotSupported" and
- // "Prepare failed.: status=0x1" if a file path String is passed to
- // setDataSource(). So unless otherwise noted, we use a
- // FileDescriptor here.
- FileInputStream fis = new FileInputStream(audioFile);
- mPlayer.reset();
- mPlayer.setDataSource(fis.getFD());
- mPlayer.prepare();
- return mPlayer;
- }
- private void changeAnotherCacheWhenEndOfCurrentCache() throws IOException {
- // 檢查當前cache剩餘時間
- long theRestTime = audioPlayer.getDuration() - audioPlayer.getCurrentPosition();
- Log.e(TAG, "theRestTime=" + theRestTime + " isChaingCacheToAnother="
- + isChaingCacheToAnother);
- if (!isChaingCacheToAnother && theRestTime <= CHANGE_CACHE_TIME) {
- isChaingCacheToAnother = true;
- Runnable r = new Runnable() {
- public void run() {
- try {
- File newCacheFile = createNewCache();
- // 設定已經從cache中複製資料,然後會刪除這個cache
- setHasMovedTheCacheToAnotherCache(true);
- transferNewCacheToAudioPlayer(newCacheFile);
- } catch (Exception e) {
- Log.e(TAG, e.getMessage()
- + ":changeAnotherCacheWhenEndOfCurrentCache() fun");
- } finally {
- deleteOldCache();
- isChaingCacheToAnother = false;
- }
- }
- };
- handler.post(r);
- }
- }
- private File createNewCache() throws Exception {
- // 將儲存網路資料的cache複製到newCache中進行播放
- String newCacheFileName = cacheFileName + (cacheFileCount++);
- File newCacheFile = new File(activity.getCacheDir(), newCacheFileName);
- Log.e(TAG, "before moveFile............the size=" + cacheFile.length());
- moveFile(cacheFile, newCacheFile);
- return newCacheFile;
- }
- private void transferNewCacheToAudioPlayer(File newCacheFile) throws Exception {
- MediaPlayer oldPlayer = audioPlayer;
- try {
- audioPlayer = createAudioPlayer(newCacheFile);
- audioPlayer.start();
- } catch (Exception e) {
- Log.e(TAG, "filename=" + newCacheFile.getName() + " size=" + newCacheFile.length());
- Log.e(TAG, e.getMessage() + " " + e.getCause() + " error start..in transfanNer..");
- }
- try {
- oldPlayer.pause();
- oldPlayer.reset();
- oldPlayer.release();
- } catch (Exception e) {
- Log.e(TAG, "ERROR release oldPlayer.");
- } finally {
- oldPlayer = null;
- }
- }
- private void deleteOldCache() {
- int oldCacheFileCount = cacheFileCount - 1;
- String oldCacheFileName = cacheFileName + oldCacheFileCount;
- File oldCacheFile = new File(activity.getCacheDir(), oldCacheFileName);
- if (oldCacheFile.exists()) {
- oldCacheFile.delete();
- }
- }
- private void errorOperator() {
- }
- }
- }
關於播放器:由於MediaPlayer的限制,我用了cache的方式來實現音訊的實時播放。即把獲取到的音訊流首先儲存到檔案中,然後當儲存到一定大小的時候就播放之,類似於QQ播放器那樣有快取的,只不過我這裡的處理得挺粗糙。程式碼寫的也挺詳細了,如果有疑問也可以提出來。
注:編碼器和播放器的編寫,我都是站在巨人的肩膀上完成的,參考了一些其他資料。
在後面一篇文章中,我將附上伺服器和客戶端的所有程式碼。
希望朋友們看完能提出意見和建議,也希望看完能有所收穫 ^_^
相關文章
- Android 音視訊錄製硬編碼實現Android
- Android短影片系統硬編碼—實現音影片編碼(三)Android
- Android短影片系統硬編碼—實現音影片編碼(二)Android
- 轉載:iOS音視訊實時採集硬體編碼iOS
- Android AudioRecord錄音 並websocket實時傳輸,AudioTrack 播放wav 音訊,Speex加密AndroidWeb音訊加密
- 視訊硬編碼(iOS端)iOS
- 新媒體編碼時代的技術:編碼與傳輸
- Android 音視訊 - MediaCodec 編解碼音視訊Android
- Android:MediaCodeC硬編碼解碼視訊,並將視訊幀儲存為圖片檔案Android
- 硬解碼播放器上如何實現截GIF功能?播放器
- Android 音視訊開發 視訊編碼,音訊編碼格式Android音訊
- Android音視訊(四)MediaCodec編解碼AACAndroid
- 使用 MediaCodec 在 Android 上進行硬解碼Android
- Android 平臺開啟硬體解碼logAndroid
- Android安全開發之淺談金鑰硬編碼Android
- 模板 vs. 硬編碼 HTMLHTML
- iOS 實時音訊採集與播放Audio Unit使用iOS音訊
- 【Codecs系列】硬體編碼器編碼引數分析
- 音視訊入門之音訊採集、編碼、播放音訊
- MediaCodec硬編碼pcm2aac
- 定時任務不在硬編碼,動態定時刷起來
- 音視訊編解碼 -- 編碼引數 CRFCRF
- 關於視訊的編解碼與傳輸技術,你想知道的都在這裡
- 編解碼持續升級,「硬」實力鑄就影片雲最優解
- 系統時鐘與硬體時鐘
- RTN實時音視訊傳輸網路
- Go JSON編碼與解碼?GoJSON
- URL編碼與解碼原理
- OpenLR 的編碼與解碼
- Android硬體加速(二)-RenderThread與OpenGL GPU渲染AndroidthreadGPU
- 音訊編碼基礎詳解音訊
- android 音訊播放 SoundPoolAndroid音訊
- C# 類對映的四種方法【解決硬編碼的問題】(工具三)C#
- android使用MediaCodec實現非同步視訊編解碼Android非同步
- 硬連結與軟連結詳解
- 漏洞簡析——CWE-259:使用硬編碼的密碼漏洞密碼
- MacBook 播放器無聲音 (排除硬體問題)Mac播放器
- PHP編碼gzdeflate與Golang解碼DEFLATEPHPGolang
- 通訊原理中碼元,碼元傳輸速率,資訊傳輸速率