MediaRecorder录制

前言

没有看过MediaRecorder其他的容器的代码,MPEG4Writer有看过一点点,就用MPEG4Writer为例来分析一下流程。其中以Audio的流程为主

MPEG4Writer是Android stagefright媒体框架下一个的封装类,我们平常录制视频调用的MediaRecorder接口类其视频录制的底层封装实现就是通过MPEG4Writer完成的,以视频为例,MPEG4Writer是视频录制的最后一环。

MediaRecorder框架

先从框架上来看一下MediaRecorder的调用流程
在这里插入图片描述

MPEG4Writer遵守 ISO 14496-12标准进行封装,在现有Android系统上录像都是录制是MP4或3GP格式,底层就是使用MPEG4Writer组合器类来完成的,它将编码后的音视频轨迹按照MPEG4规范进行封装,填入各个参数,就组合成完整的MP4格式文件。

Android系统录像封装流程主要有三个步骤:

  1. 录制开始时,写入文件头部。
  2. 录制进行时,实时写入音视频轨迹的数据块。
  3. 录制结束时,写入索引信息并更新头部参数。

索引负责描述音视频轨迹的特征,会随着音视频轨迹的存储而变化,所以通常做法会将录像文件索引信息放在音视频轨迹流后面,在媒体流数据写完(录像结束)后才能写入。

MPEG4Writer的组合功能主要由两种线程完成,一种是负责音视频数据写入封装文件的写线程(WriterThread),一种是音视频数据读取处理的轨迹线程(TrackThread)。轨迹线程一般有两个:视频轨迹数据读取线程和音频轨迹数据读取线程,而写线程只有一个,负责将轨迹线程中打包成Chunk的数据写入封装文件。

轨迹线程是以帧为单位获取数据帧(Sample),并将每帧中的信息及系统环境信息提取汇总存储在内存的trak表中,其中需要维持的信息由数据块Chunk写入文件的偏移地址Stco(Chunk Offset)、Sample与Chunk的映射关系Stsc(Sample-to-Chunk)、关键帧Stss(Sync Sample)、每一帧的持续时间Stts(Time-to-Sample)等,这些信息是跟每一帧的信息密切相关的,当录像结束时trak表会就会写入封装文件。

MediaRecorder录制流程

先看下流程图

在这里插入图片描述

第一部分,建立编码器:

如果mVideoSource == VIDEO_SOURCE_SURFACE 那么就是在prepare()的时候建立,反之是在start()的时候建立。大体流程为
StagefrightRecorder.cpp —> prepareInternal() —> setupMPEG4orWEBMRecording
—> mp4writer = new MPEG4Writer(mOutputFd);
—> setupMediaSource(&mediaSource);
—> setupVideo/AudioEncoder(mediaSource, &encoder); //encoder为mediacodecsource编码器
—> mPuller = new Puller(source);
—> writer->addSource(encoder);

MPEG4Writer.cpp —> MPEG4Writer::addSource(const sp &source)
—> Track *track = new Track(this, source, 1 + mTracks.size()); //source为encoder编码器,准备好音视频的首个元数据轨道

第二部分,启动编码器:

StagefrightRecorder.cpp —> StagefrightRecorder::start()
—> setupMPEG4orWEBMMetaData(&meta); —> mWriter->start(meta.get());
MPEG4Writer.cpp —> MPEG4Writer::start(MetaData *param);
—> writeFtypBox(param); // 实现录制文件文件头部信息的相关信息的写入操作
—> seek64(mFd, mMdatOffset, SEEK_SET); //将文件指针移动到mMdatOffset的位置
—> startWriterThread(); // 开启封装视频文件的写线程
—> startTracks(param);开启视频数据的读线程,也就是前面文件部分所说的轨迹线程 —> (*it)->start(params) // 也就是MPEG4Writer::Track::start

1、 封装视频文件的写线程 startWriterThread()
1.1 在该函数内部,在内部会真正建立新的子线程,并在子线程中执行ThreadWrapper()函数中的操作pthread_create(&mThread, &attr, ThreadWrapper, this); 。 ThreadWrapper()函数中new 了一个MPEG4Writer对象,真正的操作在threadFunc()中体现。

void *MPEG4Writer::ThreadWrapper(void *me) {
    ALOGV("ThreadWrapper: %p", me);
    MPEG4Writer *writer = static_cast<MPEG4Writer *>(me);
    writer->threadFunc();
    return NULL;
}

1.2 在threadFunc()这个函数中,将根据变量mDone (MediaRecorder.stop()时,mDone的值将为true)进行while循环,一直检测是否有数据块Chunk可写findChunkToWrite(&chunk)。轨迹线程是一直将读数据的数据往buffer中写入,buffer到了一定量后,就是chunk,这时就会通过信号量 mChunkReadyCondition来通知封装文件的写线程去检测链表,然后将检索到的Chunk数据写入文件的数据区也就是writeChunkToFile(&chunk);,当然写之前,肯定会去判断下是否真的有数据可写。

1.3 writeChunkToFile(Chunk* chunk) 轨迹线程读数据时是以数据帧Sample为单位,所以这里将Chunk写入封装文件,也是以Sample为单位,遍历整个链表,将数据写入封装文件,真正的写入操作是addSample_l(*it)

void MPEG4Writer::writeChunkToFile(Chunk* chunk) {
    ALOGV("writeChunkToFile: %" PRId64 " from %s track",
        chunk->mTimeStampUs, chunk->mTrack->getTrackType());

    int32_t isFirstSample = true;
    while (!chunk->mSamples.empty()) {
        List<MediaBuffer *>::iterator it = chunk->mSamples.begin();

        int32_t isExif;
        if (!(*it)->meta_data().findInt32(kKeyIsExif, &isExif)) {
            isExif = 0;
        }
        bool usePrefix = chunk->mTrack->usePrefix() && !isExif;

        size_t bytesWritten;
        off64_t offset = addSample_l(*it, usePrefix, isExif, &bytesWritten);

        if (chunk->mTrack->isHeic()) {
            chunk->mTrack->addItemOffsetAndSize(offset, bytesWritten, isExif);
        } else if (isFirstSample) {
            chunk->mTrack->addChunkOffset(offset);
            isFirstSample = false;
        }

        (*it)->release();
        (*it) = NULL;
        chunk->mSamples.erase(it);
    }
    chunk->mSamples.clear();
}

1.4 我们看到addSamole_l(*it) 函数,wirte写入操作,mFd 是上层设置录制的文件路径传下来的文件描述符。

off64_t MPEG4Writer::addSample_l(
        MediaBuffer *buffer, bool usePrefix, bool isExif, size_t *bytesWritten) {
    off64_t old_offset = mOffset;

    if (usePrefix) {
        addMultipleLengthPrefixedSamples_l(buffer);
    } else {
        if (isExif) {
            ::write(mFd, &kTiffHeaderOffset, 4); // exif_tiff_header_offset field
            mOffset += 4;
        }

        ::write(mFd,
              (const uint8_t *)buffer->data() + buffer->range_offset(),
              buffer->range_length());

        mOffset += buffer->range_length();
    }

    *bytesWritten = mOffset - old_offset;
    return old_offset;
}

封装文件的写入线程的操作大体就是这个样子

2、 startTracks(param) 轨迹线程

2.1 startTracks(param) 文件的录制过程中是有2条轨迹线程,一个是视频的轨迹线程,另一条则是音频的轨迹线程,在starTrack(param)中是在for 循环中start了两条轨迹线程

status_t MPEG4Writer::startTracks(MetaData *params) {
    for (List<Track *>::iterator it = mTracks.begin();
         it != mTracks.end(); ++it) {
        status_t err = (*it)->start(params);
        if (err != OK) {
            for (List<Track *>::iterator it2 = mTracks.begin();
                 it2 != it; ++it2) {
                (*it2)->stop();
            }
            return err;
        }
    }
    return OK;
}

(*it)->start(params) 将会执行status_t MPEG4Writer::Track::start(MetaData *params) {} 。在这边也是同样新建子线程,在子线程中执行轨迹线程的相应操作,真正的操作是放到了MPEG4Writer::Track::threadEntry()中去执行。

MPEG4Writer.cpp —>startTracks(MetaData *params) —> MPEG4Writer::Track::start(MetaData *params) //这里面主要分两部分

—> mSource->start(meta.get()) //动mediacodecsource编码器 —> MediaCodecSource::start(MetaData* params) // 这个地方的Source 分别对应 camerasource和audiosource —> 发送消息kWhatStart —> MediaCodecSource::onStart(MetaData *params) —> 发送消息kWhatPullerNotify 并且执行 mPuller->start(meta.get(), notify)

—> pthread_create(&mThread, &attr, ThreadWrapper, this); //创建数据线程读取编码数据并写入临时的buffer —> *MPEG4Writer::Track::ThreadWrapper —> track->threadEntry() 其中threadEntry()函数主要为:

status_t MPEG4Writer::Track::threadEntry() {
	while (!mDone && (err = mSource->read(&buffer)) == OK) {  //从编码器一直读取编码好的数据
		
		MediaBuffer *copy = new MediaBuffer(buffer->range_length());
        memcpy(copy->data(), (uint8_t *)buffer->data() + buffer->range_offset(),
                buffer->range_length());
        copy->set_range(0, buffer->range_length());  //数据拷贝到本地的buffer

		mChunkSamples.push_back(copy); // 将数据保存到mChunkSamples 并会通过bufferChunk将数据保存到ChunkInfo中然后mChunkReadyCondition.signal()
	}
}

第三部分,启动数据源:

在第二部分的时候我们有看到开启轨迹线程的时候有个mSource->start(meta.get()),我们主要通过AudioSource来看下如何启动数据源,AudioSource.cpp —> mRecord->start(); 。在这个地方,我们看下关于mRecord的构造方法

mRecord = new AudioRecord(
            inputSource, sampleRate, AUDIO_FORMAT_PCM_16_BIT,
            audio_channel_in_mask_from_count(channelCount),
            opPackageName,
            (size_t) (bufCount * frameCount),
            AudioRecordCallbackFunction,
            this,
            frameCount /*notificationFrames*/,
            AUDIO_SESSION_ALLOCATE,
            AudioRecord::TRANSFER_DEFAULT,
            AUDIO_INPUT_FLAG_NONE,
            uid,
            pid,
            NULL /*pAttributes*/,
            selectedDeviceId);
mInitCheck = mRecord->initCheck();

其中我们看到有个AudioRecordCallbackFunction函数,这个函数是一个回调函数callback_t,这个函数会注册到audioflinger中,然后在AudioRecordCallbackFunction中通过source->dataCallback(*((AudioRecord::Buffer *) info)); 拿到对应的AudioRecord的录取的音频数据。

status_t AudioSource::dataCallback(const AudioRecord::Buffer& audioBuffer) {
    MediaBuffer *buffer = new MediaBuffer(bufferSize);
    memcpy((uint8_t *) buffer->data(),
            audioBuffer.i16, audioBuffer.size);
    buffer->set_range(0, bufferSize);
    queueInputBuffer_l(buffer, timeUs);
    return OK;
}

其中queueInputBuffer_l(buffer, timeUs); 会将buffer数据存入List<MediaBuffer * > mBuffersReceived中,然后通过
mFrameAvailableCondition.signal()交给read函数处理

第四部分,获取到数据:

在第二部分启动轨迹线程的时候,mPuller->start(meta.get(), notify); —> MediaCodecSource::Puller::start —> 发送消息kWhatStart —> 当mSource->start(meta.get())成功之后会schedulePull()—> 发送消息kWhatPull —> 收到kWhatPull之后

        case kWhatPull:
        {
            queue.unlock();
            MediaBufferBase *mbuf = NULL;
            status_t err = mSource->read(&mbuf);
            queue.lock();
            break;
        }

我们可以看到mSource->read(&mbuf);这里就将第三部分mBuffersReceived中的数据读取到

第五部分:
在第二部分MediaCodecSource::onStart的时候还发送了一个kWhatPullerNotify消息,收到消息后会通知编码器往输入buffer读取数据
—> feedEncoderInputBuffers(); —> mPuller->readBuffer(&mbuf) —> queue->readBuffer(mbuf); //这儿的数据是第四部分的mReadBuffers获取的数据

—> feedEncoderInputBuffers(); —>size_t bufferIndex = *mAvailEncoderInputIndices.begin(); //获取编码器输入bufferindex,并填充输入buffer数据 —>

sp<MediaCodecBuffer> inbuf;   
status_t err = mEncoder->getInputBuffer(bufferIndex, &inbuf);
memcpy(inbuf->data(), mbuf->data(), size);
status_t err = mEncoder->queueInputBuffer(bufferIndex, 0, size, timeUs, flags);

编码器接收到数据,并编码完成会返回并回调事件:kWhatEncoderActivity,在这里处理如下事情:
1、MediaCodec::CB_INPUT_AVAILABLE 继续往inputbuffer装数据,调用feedEncoderInputBuffers接口
2、MediaCodec::CB_OUTPUT_AVAILABLE 编码器输出buffer有数据了,可以读取编码好的数据了,数据存放到了output buffer queue。
sp outbuf;
status_t err = mEncoder->getOutputBuffer(index, &outbuf);
MediaBufferBase *mbuf = new MediaBuffer(outbuf->size());
memcpy(mbuf->data(), outbuf->data(), outbuf->size());
output->mBufferQueue.push_back(mbuf);
output->mCond.signal();
mEncoder->releaseOutputBuffer(index);

第六部分,将数据写入文件:

track线程从output buffer queue读取编码好的数据存入 mSource->read(&buffer),第二步在read接口完成编码好数据读取到mChunkSamples,然后writer->threadFunc再将mChunkSamples写入到文件中。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Android 平台上,要录制系统声音需要使用 MediaProjection API 和 AudioRecord API。 首先,你需要请求用户授权使用 MediaProjection API。可以通过以下代码启动授权对话框: ```java private static final int REQUEST_MEDIA_PROJECTION = 1; private MediaProjectionManager mMediaProjectionManager; // 在 onCreate() 方法中初始化 mMediaProjectionManager mMediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); // 启动授权对话框 startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION); ``` 当用户授权后,可以在 onActivityResult() 方法中获取到 MediaProjection 对象: ```java private MediaProjection mMediaProjection; @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_MEDIA_PROJECTION && resultCode == RESULT_OK) { mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data); // 开始录制 startRecord(); } } ``` 接下来,可以使用 AudioRecord API 来录制系统声音。需要注意的是,录制系统声音需要 root 权限,或者使用系统签名的应用程序。 以下是录制系统声音的示例代码: ```java private static final int SAMPLE_RATE = 44100; private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; private AudioRecord mAudioRecord; private byte[] mBuffer; private boolean mIsRecording; private void startRecord() { int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT); mBuffer = new byte[minBufferSize]; mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.REMOTE_SUBMIX, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, minBufferSize); mAudioRecord.startRecording(); mIsRecording = true; new Thread(new Runnable() { @Override public void run() { FileOutputStream fos = null; try { fos = new FileOutputStream(new File(getExternalFilesDir(null), "record.pcm")); while (mIsRecording) { int readSize = mAudioRecord.read(mBuffer, 0, mBuffer.length); if (readSize > 0) { fos.write(mBuffer, 0, readSize); fos.flush(); } } } catch (IOException e) { e.printStackTrace(); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } } }).start(); } private void stopRecord() { mIsRecording = false; mAudioRecord.stop(); mAudioRecord.release(); mAudioRecord = null; } ``` 上述代码中使用了 AudioRecord.getMinBufferSize() 方法获取最小缓冲区大小,然后创建了一个 AudioRecord 对象,并使用 startRecording() 方法开始录制录制过程中,使用 read() 方法从 AudioRecord 中读取数据,然后将数据写入到本地文件中。 最后,使用 stop() 方法停止录制,并释放资源。 需要注意的是,上述代码中使用了 REMOTE_SUBMIX 作为录音源,这是 Android 4.4 及以上版本中新增的一种录音源,可以录制系统声音。 如果你的设备不支持 REMOTE_SUBMIX 录音源,可以尝试使用其他录音源,比如 MIC、VOICE_COMMUNICATION 等。不过,这些录音源可能无法录制系统声音,需要根据具体情况进行选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值