Android 使用Rtmp音視訊推流(002)
前言
本文介紹的是使用Android攝像頭、麥克風採集的音、視訊進行編碼。然後通過librtmp推送到流媒體伺服器上的功能。
我所使用的環境:Android Studio 2.2.3 、NDK13。
流程
使用到的Api
- 音視訊採集用到的api有:Camera、AudioRecord
- 編碼用的是系統提供的API:MediaCodec (硬編碼)
- 推送使用的開源庫:librtmp。
程式碼
ManActivity.Java
public class MainActivity extends AppCompatActivity implements SurfaceHolder .Callback2 {
static final int NAL_SLICE = 1;
static final int NAL_SLICE_DPA = 2;
static final int NAL_SLICE_DPB = 3;
static final int NAL_SLICE_DPC = 4;
static final int NAL_SLICE_IDR = 5;
static final int NAL_SEI = 6;
static final int NAL_SPS = 7;
static final int NAL_PPS = 8 ;
static final int NAL_AUD = 9;
static final int NAL_FILLER = 12;
private static final String TAG = "MainActivity";
public static final String url = "rtmp://192.168.155.1:1935/live/test";
private Button btnToggle;
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
private Camera mCamera;
private Camera.Size previewSize;
private long presentationTimeUs;
MediaCodec vencoder;
private Thread recordThread;
private boolean aLoop;
private AudioRecord mAudioRecord;
private byte[] aBuffer;
private MediaCodec aencoder;
private int aSampleRate;
private int aChanelCount;
private int colorFormat;
private MediaCodec.BufferInfo aBufferInfo = new MediaCodec.BufferInfo();
private MediaCodec.BufferInfo vBufferInfo = new MediaCodec.BufferInfo();
private boolean isPublished;
private RtmpPublisher mRtmpPublisher = new RtmpPublisher();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
btnToggle = (Button) findViewById(R.id.btn_toggle);
mSurfaceView = (SurfaceView) findViewById(R.id.surface_view);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(this);
btnToggle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
switchPublish();
}
});
}
private void switchPublish() {
if (isPublished) {
stop();
} else {
start();
}
btnToggle.setText(isPublished ? "停止" : "開始");
}
private void start() {
//初始化
mRtmpPublisher.init(url, previewSize.width, previewSize.height, 5);
isPublished = true;
initAudioDevice();
try {
vencoder = initVideoEncoder();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("video encoder init fail");
}
try {
aencoder = initAudioEncoder();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("audio encoder init fail");
}
//開啟錄音
aLoop = true;
recordThread = new Thread(fetchAudioRunnable());
presentationTimeUs = new Date().getTime() * 1000;
mAudioRecord.startRecording();
recordThread.start();
if (aencoder != null) {
aencoder.start();
}
if (vencoder != null) {
vencoder.start();
}
}
private void stop() {
isPublished = false;
mRtmpPublisher.stop();
aLoop = false;
if (recordThread != null) {
recordThread.interrupt();
}
mAudioRecord.stop();
mAudioRecord.release();
vencoder.stop();
vencoder.release();
aencoder.stop();
aencoder.release();
}
private Runnable fetchAudioRunnable() {
return new Runnable() {
@Override
public void run() {
fetchPcmFromDevice();
}
};
}
private void fetchPcmFromDevice() {
Log.d(TAG, "錄音執行緒開始");
while (aLoop && mAudioRecord != null && !Thread.interrupted()) {
int size = mAudioRecord.read(aBuffer, 0, aBuffer.length);
if (size < 0) {
Log.i(TAG, "audio ignore ,no data to read");
break;
}
if (aLoop) {
byte[] audio = new byte[size];
System.arraycopy(aBuffer, 0, audio, 0, size);
onGetPcmFrame(audio);
}
}
}
private void initAudioDevice() {
int[] sampleRates = {44100, 22050, 16000, 11025};
for (int sampleRate :
sampleRates) {
//編碼制式
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
// stereo 立體聲,
int channelConfig = AudioFormat.CHANNEL_CONFIGURATION_STEREO;
int buffsize = 2 * AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig,
audioFormat, buffsize);
if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
continue;
}
aSampleRate = sampleRate;
aChanelCount = channelConfig == AudioFormat.CHANNEL_CONFIGURATION_STEREO ? 2 : 1;
aBuffer = new byte[Math.min(4096, buffsize)];
}
}
private MediaCodec initAudioEncoder() throws IOException {
MediaCodec aencoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
aSampleRate, aChanelCount);
format.setInteger(KEY_MAX_INPUT_SIZE, 0);
format.setInteger(KEY_BIT_RATE, 1000 * 30);
aencoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return aencoder;
}
private MediaCodec initVideoEncoder() throws IOException {
// 初始化
MediaCodecInfo mediaCodecInfo = getMediaCodecInfoByType(MediaFormat.MIMETYPE_VIDEO_AVC);
colorFormat = getColorFormat(mediaCodecInfo);
MediaCodec vencoder = MediaCodec.createByCodecName(mediaCodecInfo.getName());
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,
previewSize.width, previewSize.height);
format.setInteger(KEY_MAX_INPUT_SIZE, 0);
format.setInteger(KEY_BIT_RATE, 700 * 1000);
format.setInteger(KEY_COLOR_FORMAT, colorFormat);
format.setInteger(KEY_FRAME_RATE, 20);
format.setInteger(KEY_I_FRAME_INTERVAL, 5);
vencoder.configure(format, null, null, CONFIGURE_FLAG_ENCODE);
return vencoder;
}
public static MediaCodecInfo getMediaCodecInfoByType(String mimeType) {
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
if (!codecInfo.isEncoder()) {
continue;
}
String[] types = codecInfo.getSupportedTypes();
for (int j = 0; j < types.length; j++) {
if (types[j].equalsIgnoreCase(mimeType)) {
return codecInfo;
}
}
}
return null;
}
public static int getColorFormat(MediaCodecInfo mediaCodecInfo) {
int matchedForamt = 0;
MediaCodecInfo.CodecCapabilities codecCapabilities =
mediaCodecInfo.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC);
for (int i = 0; i < codecCapabilities.colorFormats.length; i++) {
int format = codecCapabilities.colorFormats[i];
if (format >= codecCapabilities.COLOR_FormatYUV420Planar &&
format <= codecCapabilities.COLOR_FormatYUV420PackedSemiPlanar
) {
if (format >= matchedForamt) {
matchedForamt = format;
}
}
}
return matchedForamt;
}
private void initCamera() {
openCamera();
setCameraParameters();
setCameraDisplayOrientation(this, Camera.CameraInfo.CAMERA_FACING_BACK, mCamera);
try {
mCamera.setPreviewDisplay(mSurfaceHolder);
} catch (IOException e) {
e.printStackTrace();
}
mCamera.setPreviewCallbackWithBuffer(getPreviewCallback());
mCamera.addCallbackBuffer(new byte[calculateFrameSize(ImageFormat.NV21)]);
mCamera.startPreview();
}
private int calculateFrameSize(int format) {
return previewSize.width * previewSize.height * ImageFormat.getBitsPerPixel(format) / 8;
}
public static void setCameraDisplayOrientation(Activity activity,
int cameraId, android.hardware.Camera camera) {
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
int rotation = activity.getWindowManager().getDefaultDisplay()
.getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0:
degrees = 0;
break;
case Surface.ROTATION_90:
degrees = 90;
break;
case Surface.ROTATION_180:
degrees = 180;
break;
case Surface.ROTATION_270:
degrees = 270;
break;
}
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
camera.setDisplayOrientation(result);
}
private void setCameraParameters() {
Camera.Parameters parameters = mCamera.getParameters();
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
for (Camera.Size size : supportedPreviewSizes
) {
if (size.width >= 320 && size.width <= 720) {
previewSize = size;
Log.d(TAG, String.format("find preview size width=%d,height=%d", previewSize.width,
previewSize.height));
break;
}
}
int[] destRange = {25 * 1000, 45 * 1000};
List<int[]> supportedPreviewFpsRange = parameters.getSupportedPreviewFpsRange();
for (int[] range : supportedPreviewFpsRange
) {
if (range[PREVIEW_FPS_MIN_INDEX] <= 45 * 1000 && range[PREVIEW_FPS_MAX_INDEX] >= 25 * 1000) {
destRange = range;
Log.d(TAG, String.format("find fps range :%s", Arrays.toString(destRange)));
break;
}
}
parameters.setPreviewSize(previewSize.width, previewSize.height);
parameters.setPreviewFpsRange(destRange[PREVIEW_FPS_MIN_INDEX],
destRange[PREVIEW_FPS_MAX_INDEX]);
parameters.setFocusMode(FOCUS_MODE_AUTO);
parameters.setPreviewFormat(ImageFormat.NV21);
parameters.setRotation(onOrientationChanged(0));
mCamera.setParameters(parameters);
}
public int onOrientationChanged(int orientation) {
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, info);
orientation = (orientation + 45) / 90 * 90;
int rotation = 0;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
rotation = (info.orientation - orientation + 360) % 360;
} else { // back-facing camera
rotation = (info.orientation + orientation) % 360;
}
return rotation;
}
private void openCamera() {
if (mCamera == null) {
try {
mCamera = Camera.open();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "開啟攝像頭失敗", Toast.LENGTH_SHORT).show();
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
throw new RuntimeException("開啟攝像頭失敗", e);
}
}
}
@Override
protected void onResume() {
super.onResume();
initCamera();
}
@Override
protected void onPause() {
super.onPause();
if (mCamera != null) {
mCamera.setPreviewCallbackWithBuffer(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
@Override
public void surfaceRedrawNeeded(SurfaceHolder holder) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
initCamera();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
public Camera.PreviewCallback getPreviewCallback() {
return new Camera.PreviewCallback() {
byte[] dstByte = new byte[calculateFrameSize(ImageFormat.NV21)];
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (data != null) {
if (isPublished) {
// data 是Nv21
if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) {
Yuv420Util.Nv21ToYuv420SP(data, dstByte, previewSize.width, previewSize.height);
} else if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar) {
Yuv420Util.Nv21ToI420(data, dstByte, previewSize.width, previewSize.height);
} else if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible) {
// Yuv420_888
} else if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar) {
// Yuv420packedPlannar 和 yuv420sp很像
// 區別在於 加入 width = 4的話 y1,y2,y3 ,y4公用 u1v1
// 而 yuv420dp 則是 y1y2y5y6 共用 u1v1
//http://blog.csdn.net/jumper511/article/details/21719313
//這樣處理的話顏色核能會有些失真。
Yuv420Util.Nv21ToYuv420SP(data, dstByte, previewSize.width, previewSize.height);
} else {
System.arraycopy(data, 0, dstByte, 0, data.length);
}
onGetVideoFrame(dstByte);
}
camera.addCallbackBuffer(data);
} else {
camera.addCallbackBuffer(new byte[calculateFrameSize(ImageFormat.NV21)]);
}
}
};
}
private void onGetVideoFrame(byte[] dstByte) {
ByteBuffer[] inputBuffers = vencoder.getInputBuffers();
ByteBuffer[] outputBuffers = vencoder.getOutputBuffers();
int inputBufferId = vencoder.dequeueInputBuffer(-1);
if (inputBufferId >= 0) {
// fill inputBuffers[inputBufferId] with valid data
ByteBuffer bb = inputBuffers[inputBufferId];
bb.clear();
bb.put(dstByte, 0, dstByte.length);
long pts = new Date().getTime() * 1000 - presentationTimeUs;
vencoder.queueInputBuffer(inputBufferId, 0, dstByte.length, pts, 0);
}
for (; ; ) {
int outputBufferId = vencoder.dequeueOutputBuffer(vBufferInfo, 0);
if (outputBufferId >= 0) {
// outputBuffers[outputBufferId] is ready to be processed or rendered.
ByteBuffer bb = outputBuffers[outputBufferId];
onEncodedAvcFrame(bb, vBufferInfo);
vencoder.releaseOutputBuffer(outputBufferId, false);
}
if (outputBufferId < 0) {
break;
}
}
}
private void onGetPcmFrame(byte[] data) {
ByteBuffer[] inputBuffers = aencoder.getInputBuffers();
ByteBuffer[] outputBuffers = aencoder.getOutputBuffers();
int inputBufferId = aencoder.dequeueInputBuffer(-1);
if (inputBufferId >= 0) {
ByteBuffer bb = inputBuffers[inputBufferId];
bb.clear();
bb.put(data, 0, data.length);
long pts = new Date().getTime() * 1000 - presentationTimeUs;
aencoder.queueInputBuffer(inputBufferId, 0, data.length, pts, 0);
}
for (; ; ) {
int outputBufferId = aencoder.dequeueOutputBuffer(aBufferInfo, 0);
if (outputBufferId >= 0) {
// outputBuffers[outputBufferId] is ready to be processed or rendered.
ByteBuffer bb = outputBuffers[outputBufferId];
onEncodeAacFrame(bb, aBufferInfo);
aencoder.releaseOutputBuffer(outputBufferId, false);
}
if (outputBufferId < 0) {
break;
}
}
}
private void onEncodedAvcFrame(ByteBuffer bb, MediaCodec.BufferInfo vBufferInfo) {
int offset = 4;
//判斷幀的型別
if (bb.get(2) == 0x01) {
offset = 3;
}
int type = bb.get(offset) & 0x1f;
if (type == NAL_SPS) {
//[0, 0, 0, 1, 103, 66, -64, 13, -38, 5, -126, 90, 1, -31, 16, -115, 64, 0, 0, 0, 1, 104, -50, 6, -30]
//打印發現這裡將 SPS幀和 PPS幀合在了一起傳送
// SPS為 [4,len-8]
// PPS為後4個位元組
//so .
byte[] pps = new byte[4];
byte[] sps = new byte[vBufferInfo.size - 12];
bb.getInt();// 拋棄 0,0,0,1
bb.get(sps, 0, sps.length);
bb.getInt();
bb.get(pps, 0, pps.length);
Log.d(TAG, "解析得到 sps:" + Arrays.toString(sps) + ",PPS=" + Arrays.toString(pps));
mRtmpPublisher.sendSpsAndPps(sps, sps.length, pps, pps.length,
vBufferInfo.presentationTimeUs / 1000);
} else {
byte[] bytes = new byte[vBufferInfo.size];
bb.get(bytes);
mRtmpPublisher.sendVideoData(bytes, bytes.length,
vBufferInfo.presentationTimeUs / 1000);
}
}
private void onEncodeAacFrame(ByteBuffer bb, MediaCodec.BufferInfo aBufferInfo) {
// 1.界定符 FF F1
// 2.加上界定符的前7個位元組是幀描述資訊
// 3.AudioDecoderSpecificInfo 長度為2個位元組如果是44100 改值為0x1210
//http://blog.csdn.net/avsuper/article/details/24661533<
相關推薦
Android 使用Rtmp音視訊推流(002)
前言 本文介紹的是使用Android攝像頭、麥克風採集的音、視訊進行編碼。然後通過librtmp推送到流媒體伺服器上的功能。 我所使用的環境:Android Studio 2.2.3 、NDK13。 流程 使用到的Api 音視訊採集用到的api有:Camera、AudioRecord編
Android WebRTC 音視訊開發總結(二)
1 public void setTrace(boolean enable, VideoEngine.TraceLevel traceLevel) { 2 if (enable) { 3 vie.setTraceFile("/sdcard/trace.txt", f
Android 音視頻深入 十五 FFmpeg 實現基於Rtmp協議的推流(附源碼下載)
音視頻 FFmpeg Rtmp 推流 源碼地址https://github.com/979451341/Rtmp 1.配置RTMP服務器 這個我不多說貼兩個博客分別是在mac和windows環境上的,大家跟著弄MAC搭建RTMP服務器https://www.jianshu.com/p/6fce
1小時學會:最簡單的iOS直播推流(九)flv 編碼與音視訊時間戳同步
最簡單的iOS 推流程式碼,視訊捕獲,軟編碼(faac,x264),硬編碼(aac,h264),美顏,flv編碼,rtmp協議,陸續更新程式碼解析,你想學的知識這裡都有,願意懂直播技術的同學快來看!! 前文介紹瞭如何獲取音視訊的aac/h2
Nginx-RTMP推流(video)
Camera 採集資料 Camera負責採集資料,把採集來的資料交給 X264進行編碼打包給RTMP進行推流, Camera採集來的資料是NV21, 而X264編碼的輸入資料格式為I420格式。 NV21和I420都是屬於YUV420格式。而NV21是一種two-plane模式,即Y和UV分為兩個Pla
Nginx-RTMP推流(audio)
需要文中完整程式碼的可以前往Github上獲取,順便給個star唄。 AAC編碼 推送音訊跟推送視訊差不多,經過資料採集,編碼,然後通過RTMP推流。資料採集通常有兩種方式,一種是Java層的AudioRecord,另一種是native層opensl es;採集完後就是編碼,相比視訊比較簡單,編碼庫這
Android端實現多人音視訊聊天應用(一)
本文轉載於資深Android開發者“東風玖哥”的部落格。 本系列文章分享了基於Agora SDK 2.1實現多人視訊通話的實踐經驗。 轉載已經過原作者許可。原文地址 自從2016年,鼓吹“網際網路寒冬”的論調甚囂塵上,2017年亦有愈演愈烈之勢。但連麥直播、線上抓娃
一個直播例子:快速整合iOS基於RTMP的視訊推流
效果圖 iTools有點卡, 但是推到伺服器倒是很快的. 推流 前言 這篇blog是iOS視訊直播初窺:<喵播APP>的一個補充. 因為之前傳到github上的專案中沒有整合視訊的推流.有很多朋友簡信和微博上問我推流這部分怎麼實現的. 所以, 我重新集成了RTMP的推流, 合併到了
iOS:基於RTMP的視訊推流
iOS基於RTMP的視訊推流 一、基本介紹 iOS直播一出世,立馬火熱的不行,各種直播平臺如雨後春筍,正因為如此,也同樣帶動了直播的技術快速發展,在IT界精通直播技術的猴子可是很值錢的。直播技術涉及的知識面很廣,最主要的大概就是這幾個:軟硬解碼.h264、美顏處理、推流RTMP、拉流播放、視訊錄製、傳送彈幕
深入理解Android音視訊同步機制(四)MediaSync的使用與原理
MedaiSync是android M新加入的API,可以幫助應用視音訊的同步播放,如同官網介紹的 From Andriod M: MediaSync: class which helps applications to synchronously r
JMeter擴充套件Java請求實現WebRTC本地音視訊推流壓測指令碼
WebRTC是Web Real-Time Communication縮寫,指網頁即時通訊,是一個支援Web瀏覽器進行實時語音或視訊對話的API,實現了基於網頁的視訊會議,比如聲網的Agora Web SDK就是基於WebRTC實現音視訊通訊的。與HTTP不同,WebRTC應用的主要壓力是碼流,JMeter沒有
Android視音頻編碼器(2)——cameraYUV、AudioRecordPCM分別編碼後muxer成mp4
效率 androi mar 視音頻 pop 采集 con 文章 cpu 參考下面這篇文章: http://blog.csdn.net/a992036795/article/details/54286654 一、前言 上一篇文章我講到,我用libx264對視頻
音視訊開發系列(一)
人類的五官能夠直接感受聲音和影象,但計算機只能認識二進位制。在音視訊的開發過程中,我們必須將聲音和影象編碼為二進資料流,才能讓計算機識別,進而加工處理、傳輸和儲存;計算機上為人類服務的,儲存的音視訊資料被使用者獲取後,需要重新解碼為人類能夠直接感受的聲音、影象。 大連哪個醫院看婦
從零開始學習音視訊程式設計技術(四) FFMPEG的使用
零開始學習音視訊程式設計技術(四) FFMPEG的使用 音視訊開發中最常做的就是編解碼的操作了,以H.264為例:如果想要自己實現編碼h.264,需要對H.264非常的瞭解,首先需要檢視H.264的文件,這個文件好像說是三百多頁(本人並沒有看過)。 想到這
從零開始學習音視訊程式設計技術(一) 視訊格式講解(學習筆記)
/* 該型別部落格為學習時載錄筆記,加上自己對一些不理解部分自己的理解。會涉及其他博主的博文的摘錄,會標註出處 */ ==========================================================================
從零開始學習音視訊程式設計技術(二) 音訊格式講解
1. 音訊簡介 前面我們說過視訊有一個每秒鐘採集多少張的概念,這就叫做視訊的幀率。 和視訊的幀率一樣的道理,聲音也有一個頻率,叫做取樣率。 人對頻率的識別範圍是 20HZ - 20000HZ, 如果每秒鐘能對聲音做 20000 個取樣, 回放
百度雲音視訊直播服務(LSS)的使用流程
音視訊直播LSS(Live Streaming Service)是一個直播PaaS服務平臺,旨在幫助企業及個人開發者快速搭建自己的直播平臺及應用,關於LSS的相關介紹請採參考百度雲官網指導文件:https://cloud.baidu.com/doc/LSS/ProductDe
基於webrtc多人音視訊的研究(一)
基於webrtc多人音視訊的研究 眾所周知,WebRTC非常適合點對點(即一對一)的音視訊會話。然而,當我們的客戶要求超越一對一,即一對多、多對一設定多對多的解決方案或者服務,那麼問題就來了:“我們應該採用什麼樣的架構?” 。簡單的呢有人會考慮copy多個p2p就完
從零開始學習音視訊程式設計技術(七) FFMPEG Qt視訊播放器之SDL的使用
前面介紹了使用FFMPEG+Qt解碼視訊並顯示。 現在我們就著手給它加上聲音播放。 播放聲音有很多種方式: 以windows系統為例,可以使用如下方法播放音訊: 1.直接呼叫系統API的wavein、waveout等函式 2.使用directsound播放
從零開始學習音視訊程式設計技術(一) 視訊格式講解
所謂視訊,其實就是將一張一張的圖片連續的放出來,就像放幻燈片一樣,由於人眼的惰性,因此只要圖片的數量足夠多,就會覺得是連續的動作。 所以,只需要將一張一張的圖片儲存下來,這樣就可以構成一個視訊了。 但是,由於目前網路和儲存空間的限制,直接儲存圖片顯然不可行。