android录制avi视频

        大家都知道,在android上,现在录制支持的主流视频是mp4,mpeg2,也就是流媒体,也写了个大概,但是支持的不完全,原生的用来录流媒体会有种种问题。

        当然,在大多数情况下,mp4视频是够用了的,一般人录制也就只需要mp4格式就够了,用户并不关心你录的什么格式的,只要能播放就ok。但是在某些特定的行业,或者特定的需求下,mp4就不够用了。

        比如在车载设备方面。因为mp4录制视频不是实时写的,它是录制一定的chunk,然后过一段时间就去一次性写一段内容。然后再在结束录制的时候,去写文件结构的各个box。正常情况下是没什么事,但是一旦突然撞车了,设备失去了电力供应,突然关机了,那么这个视频就会因为没有来得及写必要的控制box,而无法播放。

        再说说avi,avi其实跟Mp4有一点相同,也是在停止录像时才去写文件头结构,这个头结构里主要包括了文件的音频视频索引。不过avi比mp4好一点的就是,因为avi里存的数据可以是直接从摄像头里出来的原生的mjpeg图片,每一帧都是一张完整的图片。它以FFD8开头,以FFD9结尾,如果你将这一代码代码拷出来保存起来,是可以用画图软件直接打开的。这样的好处就是,哪怕突然关机,没来得及写索引,有一部份播放器也是可以播放的,并且也可以后期通过一些软件来修复avi的索引。

        当然,上面说了这么多,都不是我们要在android系统里增加录制avi视频功能的主要原因。我们要开发这个功能的主要原因,是因为功耗。以usbcamera为例,目前我们平台上的usbcamera输出的原始图像是mjpeg格式的。这样的格式的数据,是不能够用来预览的,它需要通过libyuv库,调用MJPGToI420函数,将mjpg数据转成I420格式的,然后再调用libyuv的I420ToABGR函数,将i420格式的转成abgr的。这样的数据才可以用来送显,才可以正常预览。

        如果仅仅只是预览,也还无所谓。但如果还需要录像,比如行车记录仪,大多都是一边预览一边录像的。这种情况下,如果用Mp4格式录制的话,这个功耗就是相当的大了。它不仅仅要经历前面的mjpg->I420->ABGR,而且在录制视频的时候,还需要将这里转好了的数据,关给mediacodec去编码成h264的,然后camerasource再从mediacodec里去取编码好了的数据,按照mp4的要求来写文件。这个过程,一共经历了三次编码转换,这个开销是相当的大的。

        如果想要降低这个开销,最好的一个办法,就是将录制的视频格式由mp4格式的改成avi格式的。因为avi视频录制时,可以将从usbcamera里采集到的数据,直接写到avi文件里,中间不需要经过任何编码。这么一来,开销自然就大大的降低了。

        有了这个目标,我们再说说思路。现在网上一部份的录制avi的功能,是直接放在app里做的,从onPreviewFrame或者ImageReader里取数据流。不过这两个java层的函数接口,并不能直接获取mjpeg格式的图片,还是需要转换几次才能得到mjpeg的格式的数据,然后写avi。同时,因为数据已经到了java层,效率没有在C++层处理那么高。并且,我们做方案的,不应该把这些功能丢给应用去做。我们应该在底层就写好了接口,做应用的只需要在应用上面指定录制的视频格式即可。这样才方便客户在上面进行二次开发。

        要理清这思路,需要先明白视频录制的一个从上到下层的过程。从app往下来讲,录制视频时,需要先在app上new MediaRecorder这个对像,然后setCamera->setAudioSource->setVideoSource->setProfile->setOutputFile->prepare->start(),然后就开始录相了。

        这其中,用户需要关心的,主要是setProfile函数,在这个函数,传进去的CamcorderProfile参数里,可以设置需要录制的视频类型,视频编码,以及音频参数等。如果客户需要录制avi视频,就需要指定fileFormat这个参数为10,videoCodec为5.  这里的10和5,是我在系统里为avi新增的。10表示录制avi格式的视频,5表示视频的编码为mjpeg的。具体见下面代码:

        mMediaRecorder.setCamera(camera);
        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

		
		//add by xuhui for avi 
		mProfile.fileFormat = 10;//avi  
		mProfile.videoCodec = 5; //V4L2_PIX_FMT_MJPEG
		mProfile.audioBitRate = 96000;
		mProfile.audioCodec = 3;// AUDIO_ENCODER_AAC;   
		mProfile.audioSampleRate = 44100;
		mProfile.audioChannels = 1;  
        //add end 
		 
		 
        mMediaRecorder.setProfile(mProfile); 

这个10定义在frameworks\av\include\media\mediarecorder.h以及frameworks\base\media\java\android\media\MediaRecorder.java里,见代码:

enum output_format {
    OUTPUT_FORMAT_DEFAULT = 0,
    OUTPUT_FORMAT_THREE_GPP = 1,
    OUTPUT_FORMAT_MPEG_4 = 2,


    OUTPUT_FORMAT_AUDIO_ONLY_START = 3, // Used in validating the output format.  Should be the
                                        //  at the start of the audio only output formats.

    /* These are audio only file formats */
    OUTPUT_FORMAT_RAW_AMR = 3, //to be backward compatible
    OUTPUT_FORMAT_AMR_NB = 3,
    OUTPUT_FORMAT_AMR_WB = 4,
    OUTPUT_FORMAT_AAC_ADIF = 5,
    OUTPUT_FORMAT_AAC_ADTS = 6,

    OUTPUT_FORMAT_AUDIO_ONLY_END = 7, // Used in validating the output format.  Should be the
                                      //  at the end of the audio only output formats.

    /* Stream over a socket, limited to a single stream */
    OUTPUT_FORMAT_RTP_AVP = 7,

    /* H.264/AAC data encapsulated in MPEG2/TS */
    OUTPUT_FORMAT_MPEG2TS = 8,

    /* VP8/VORBIS data in a WEBM container */
    OUTPUT_FORMAT_WEBM = 9,

	OUTPUT_FORMAT_AVI = 10,

    OUTPUT_FORMAT_LIST_END // must be last - used to validate format type
};
frameworks\av\include\media\mediarecorder.h
enum video_encoder {
    VIDEO_ENCODER_DEFAULT = 0,
    VIDEO_ENCODER_H263 = 1,
    VIDEO_ENCODER_H264 = 2,
    VIDEO_ENCODER_MPEG_4_SP = 3,
    VIDEO_ENCODER_VP8 = 4,
    VIDEO_ENCODER_MJPEG = 5,

    VIDEO_ENCODER_LIST_END // must be the last - used to validate the video encoder type
};
//frameworks\base\media\java\android\media\MediaRecorder.java
    public final class OutputFormat {
      /* Do not change these values without updating their counterparts
       * in include/media/mediarecorder.h!
       */
        private OutputFormat() {}
        public static final int DEFAULT = 0;
        /** 3GPP media file format*/
        public static final int THREE_GPP = 1;
        /** MPEG4 media file format*/
        public static final int MPEG_4 = 2;

        /** The following formats are audio only .aac or .amr formats */

        /**
         * AMR NB file format
         * @deprecated  Deprecated in favor of MediaRecorder.OutputFormat.AMR_NB
         */
        public static final int RAW_AMR = 3;

        /** AMR NB file format */
        public static final int AMR_NB = 3;

        /** AMR WB file format */
        public static final int AMR_WB = 4;

        /** @hide AAC ADIF file format */
        public static final int AAC_ADIF = 5;

        /** AAC ADTS file format */
        public static final int AAC_ADTS = 6;

        /** @hide Stream over a socket, limited to a single stream */
        public static final int OUTPUT_FORMAT_RTP_AVP = 7;

        /** @hide H.264/AAC data encapsulated in MPEG2/TS */
        public static final int OUTPUT_FORMAT_MPEG2TS = 8;

        /** VP8/VORBIS data in a WEBM container */
        public static final int WEBM = 9;

		public static final int AVI = 10;
    };

        好了,应用层需要做的事情就完成了。如果我们底层封装得好,应用层,只需要在他们的代码里,设置fileFormat和videoCodec这两个参数。是不是特别友好?当然,这里设好后,我们在framework层还需要做对应的处理。

        应用层在调用mMediaRecorder.prepare()时,会调用到frameworks\av\media\libmediaplayerservice\StagefrightRecorder.cpp这个文件里的StagefrightRecorder::prepareInternal()函数。在这里,会通过用户设置的mOutputFormat来选择安装哪个打包器。默认的mOutputFormat是2,也就是OUTPUT_FORMAT_MPEG_4,对应的,就会选择mpeg4的打包器setupMPEG4orWEBMRecording()。如果应用设置的是10,也就是刚刚我们新增的OUTPUT_FORMAT_AVI,那么就会选择setupAVIRecording()。见代码:

status_t StagefrightRecorder::prepareInternal() {
    ALOGD("prepare mOutputFormat is %d", mOutputFormat);
    if (mOutputFd < 0) {
        ALOGE("Output file descriptor is invalid");
        return INVALID_OPERATION;
    }

    // Get UID here for permission checking
    mClientUid = IPCThreadState::self()->getCallingUid();

    status_t status = OK;
	
    switch (mOutputFormat) {
        case OUTPUT_FORMAT_DEFAULT:
        case OUTPUT_FORMAT_THREE_GPP:
        case OUTPUT_FORMAT_MPEG_4:
        case OUTPUT_FORMAT_WEBM:
            status = setupMPEG4orWEBMRecording();
            break;

        case OUTPUT_FORMAT_AMR_NB:
        case OUTPUT_FORMAT_AMR_WB:
            status = setupAMRRecording();
            break;

        case OUTPUT_FORMAT_AAC_ADIF:
        case OUTPUT_FORMAT_AAC_ADTS:
            status = setupAACRecording();
            break;

        case OUTPUT_FORMAT_RTP_AVP:
            status = setupRTPRecording();
            break;

        case OUTPUT_FORMAT_MPEG2TS:
            status = setupMPEG2TSRecording();
            break;

		case OUTPUT_FORMAT_AVI:
		    status = setupAVIRecording();
		    break;

        default:
            ALOGE("Unsupported output file format: %d", mOutputFormat);
            status = UNKNOWN_ERROR;
            break;
    }

    return status;
}

这个setupAVIRecording也是我新增的函数,见代码:

//add by xuhui
status_t StagefrightRecorder::setupAVIRecording() {

    ALOGD("setupAVIRecording start mOutputFormat is %d", mOutputFormat);
    mWriter.clear();
    mTotalBitRate = 0;

    status_t err = OK;
    sp<MediaWriter> writer;
    sp<AVIWriter> aviWriter;
    writer = aviWriter = new AVIWriter(mOutputFd);

    if (mVideoSource < VIDEO_SOURCE_LIST_END) {
        setDefaultVideoEncoderIfNecessary();

        sp<MediaSource> mediaSource;
        err = setupMediaSource(&mediaSource);
        if (err != OK) {
            return err;
        }

		//我们avi视频不需要编码,所以不安装编码器。
      /*  sp<MediaSource> encoder;
        err = setupVideoEncoder(mediaSource, &encoder);
        if (err != OK) {
            return err;
        }*/
	    //也不需要将编码好的数据当成数据源
        //writer->addSource(encoder);
		writer->addSource(mediaSource);
        mTotalBitRate += mVideoBitRate;
    }

    if (mOutputFormat != OUTPUT_FORMAT_WEBM) {
        // Audio source is added at the end if it exists.
        // This help make sure that the "recoding" sound is suppressed for
        // camcorder applications in the recorded files.
        // TODO Audio source is currently unsupported for webm output; vorbis encoder needed.
        // disable audio for time lapse recording
        bool disableAudio = mCaptureFpsEnable && mCaptureFps < mFrameRate;
		ALOGD("setupAVIRecording disableAudio is %d", disableAudio);
        if (!disableAudio && mAudioSource != AUDIO_SOURCE_CNT) {
            if (!(mAudioSource == AUDIO_SOURCE_RECORD_NO_AUDIO)) {
				ALOGD("setupAVIRecording add audio 1");
                err = setupAudioEncoder(writer);
			    ALOGD("setupAVIRecording add audio 2 err is %d", err);
                if (err != OK) return err;
                mTotalBitRate += mAudioBitRate;
            }
        }
    }
    writer->setListener(mListener);
    mWriter = writer;
	ALOGD("setupAVIRecording end");
    return OK;
}
//add end

        上面最重要的一行代码是:writer->addSource(mediaSource),这个mediaSource是通过下面的代码来创建的:

status_t StagefrightRecorder::setupMediaSource(
                      sp<MediaSource> *mediaSource) {
    if (mVideoSource == VIDEO_SOURCE_DEFAULT
            || mVideoSource == VIDEO_SOURCE_CAMERA) {
        sp<CameraSource> cameraSource;
        status_t err = setupCameraSource(&cameraSource);
		ALOGD("StagefrightRecorder::setupMediaSource err is %d", err);
        if (err != OK) {
            return err;
        }
        *mediaSource = cameraSource;
    } else if (mVideoSource == VIDEO_SOURCE_SURFACE) {
        *mediaSource = NULL;
    } else {
        return INVALID_OPERATION;
    }
    return OK;
}

        在setupCameraSource里调用到了下面的代码:

        *cameraSource = CameraSource::CreateFromCamera(
                mCamera, mCameraProxy, mCameraId, mClientName, mClientUid,
                videoSize, mFrameRate,
                mPreviewSurface);

        这些代码的意思就是,我这个AVIWriter的数据源,就是上面创建的cameraSource。录制mp4时,写到mp4里的每一帧数据,就是从这个cameraSource里的read里读取的。注意,在录制mp4时,cameraSource里的read读取到的数据,不是直接从摄像头里取出来的数据,而是从编码器mediacoderc里read到的已经编码成了h264了的数据。

        另外需要强调的是,camera采集到数据后往上传跨进程传递时,不是传递的一大段实际的内存数组,那样的话,内存开销太大了。而是通过共享内存来传递的。以cameraSouce为例,cameraSource里的read,是由dataCallbackTimestamp调用的(dataCallbackTimestamp是由camera2Client里一层层的调过来的),而dataCallbackTimestamp里的参数const sp<IMemory> &data,就是这个共享内存传送的指针。

        但是,大家特别需要注意的是,这个data->pointer()指向的并不是实际的视频帧数据所在的地址,data->size()返回的,也并不是实际视频帧数据的大小。这个共享内存,传递的只是一个封装过后的VideoNativeMetadata指针,size也是这个结构体的大小。这个结构体中的pBuffer指针,指向了ANativeWindowBuffer结构体指针。然后在ANativeWindowBuffer这个结构体的成员handle->data[0],才是指向了实际的视频帧数据的共享内存句柄。可以通过如下方式来获取到实际的共享内存里的视频帧:

		    VideoNativeMetadata *payload =
		            reinterpret_cast<VideoNativeMetadata*>(frame->pointer());			
			payload->pBuffer = (ANativeWindowBuffer*)(((uint8_t*)payload->pBuffer));
			//就是下面这种操作,payload->pBuffer->handle->data[0]这个存的就是在usbcamera里申请内存时的fd句柄。下面这样取是对的
            void *addr = mmap(NULL, 614400 , PROT_READ | PROT_WRITE, MAP_SHARED, dup(payload->pBuffer->handle->data[0]), 0);
            ret = AVI_write_frame(mOwner->avi_fd, (char*)(addr), 614400);
			ALOGD("avi AVI_write_frame ret is %d\n",ret);
			ret = munmap(addr, 614400);

        这样就可以取到usbcamera里传上来的视频数据帧了。如果这时usbcamera里读到的帧没有经过MJPGToI420->I420ToABGR转换,直接是以mjpeg出来的话,那么在这里,就可以通过AVI_writer_frame来写avi文件了。当然,事实并没有这么美好。如果在usbcamera里读到的数据不经过转换,就直接丢上来的话,那么预览那里,就会啥都显示不了,是一片黑。因为预览显示那里,只支持abgr的数据,不支持mjpg的。所以,从这里直接取数据,然后写avi的想法,可以丢弃了。我们需要另外想办法。

        不过我们可以先分析下这里从传进来的共享内存指针const sp<IMemory> &data里取数据的过程。这里的frame->pointer指向的,是一个 VideoNativeMetadata结构体指针。也就是说,camera从hal层到framework层,传递的不是实际的帧数据,而是这个结构体指针。很显然,实际的帧数据,就应该被封在这个结构体内。我们再来看看这个结构体是怎么定义的:

//定义在frameworks/native/include/media/hardware/HardwareAPI.h这个文件里
struct VideoNativeMetadata {
    MetadataBufferType eType;               // must be kMetadataBufferTypeANWBuffer
#ifdef OMX_ANDROID_COMPILE_AS_32BIT_ON_64BIT_PLATFORMS
    OMX_PTR pBuffer;
#else
    struct ANativeWindowBuffer* pBuffer;
#endif
    int nFenceFd;                           // -1 if unused
};

        这结构体中有个struct ANativeWindowBuffer* pBuffer;  看来我们的帧数据又被封到这个里面去了,继续看这个结构体的定义:

typedef struct ANativeWindowBuffer  
{  
#ifdef __cplusplus  
    ANativeWindowBuffer() {  
        // ANDROID_NATIVE_BUFFER_MAGIC的值是"_bfr"  
        common.magic = ANDROID_NATIVE_BUFFER_MAGIC;  
        common.version = sizeof(ANativeWindowBuffer);  
        memset(common.reserved, 0, sizeof(common.reserved));  
    }     
  
    // Implement the methods that sp<ANativeWindowBuffer> expects so that it  
    // can be used to automatically refcount ANativeWindowBuffer's.  
    // 调用common,也就是android_native_base_t的incRef和decRef函数,具体函数是什么还不知道  
    void incStrong(const void* /*id*/) const {  
        common.incRef(const_cast<android_native_base_t*>(&common));  
    }     
    void decStrong(const void* /*id*/) const {  
        common.decRef(const_cast<android_native_base_t*>(&common));  
    }     
#endif  
  
    // common的incRef和decRef还没有明确是什么  
   struct android_native_base_t common;  
  
    int width;  
    int height;  
    int stride;  
    int format;  
    int usage;  
  
    void* reserved[2];  
    // buffer_handle_t是指向sturct native_handle, native_handle_t, struct private_handle_t的指针.  
    buffer_handle_t handle;  
  
    void* reserved_proc[8];  
} ANativeWindowBuffer_t;  
// Old typedef for backwards compatibility.  
typedef ANativeWindowBuffer_t android_native_buffer_t;  

        这里有一个buffer_handle_t handle;  这个handle就是我们上面用到的payload->pBuffer->handle, 我们的帧数据还没有暴露出来,我们仍要进这个handle里去查看。

//定义在system/core/include/system/window.h
typedef const native_handle_t* buffer_handle_t;  

//定义在system/core/include/cutils/native_handle.h
typedef struct native_handle  
{  
    int version;        /* sizeof(native_handle_t) */  
    int numFds;         /* number of file-descriptors at &data[0] */  
    int numInts;        /* number of ints at &data[numFds] */  
    int data[0];        /* numFds + numInts ints */  
} native_handle_t;  

        再这个native_handle结构里,我们看到了一个data[0],它就是我们上面用到的payload->pBuffer->handle->data[0],这个data[0]究竟是个什么东西呢?要清明白这个,我们还需要看看gralooc分配的buffer是怎么去描述怎么在进程间传递的:

         gralloc分配的buffer都可以用一个private_handle_t来描述,同时也可以用一个native_handle来描述.在不同的平台的实现上,private_handle_t可能会有不同的定义,所以private_handle_t在各个模块之间传递的时候很不方便,而如果用native_handle的身份来传递,就可以消除平台的差异性.在HardwareComposer中,由SurfaceFlinger传给hwc的handle即是native_handle类型,而hwc作为平台相关的模块,他需要知道native_handle中各个字段的具体含义,所以hwc往往会将native_handle指针转化为private_handle_t指针来使用.  我们camera里分配的帧数据buffer,也是这个原理。现在我们看看private_handle_t的结构:

#ifdef __cplusplus  
//在c++编译环境下private_handle_t继承于native_handle  
struct private_handle_t : public native_handle {  
#else  
//在c编译环境下,private_handle_t的第一个成员是native_handle类型,其实和c++的继承是一个意思,  
//总之就是一个指向private_handle_t的指针同样也可以表示一个指向native_handle的指针.  
struct private_handle_t {  
    struct native_handle nativeHandle;  
#endif  
    // file-descriptors  
    int     fd;   
    // ints  
    int     magic;  
    int     flags;  
    int     size;  
    int     offset;  
    // 因为native_handle的data成员是一个大小为0的数组,所以data[0]其实就是指向了fd,data[1]指向magic,以此类推.  
    // 上面提到我们可以把native_handle看成是一个纯虚的基类,那么在private_handle_t这个派生类中,numFds=1 numInts=4.  
    ...  
}  

        为了让大家看得更明白,我引用网上一张图:


        这下大家应该都看明白了,我们的这个data[0],也就是上面用到的payload->pBuffer->handle->data[0],它实际指向的,就是我们帧数据buff所申请的共享内存的句柄。有了这个句柄,我们dup一下,就可以将它mmap到我们本地的进程中来,然后就可以像操作在本地申请的内存一样来操作它了。见代码:

            void *addr = mmap(NULL, 614400 , PROT_READ | PROT_WRITE, MAP_SHARED, dup(payload->pBuffer->handle->data[0]), 0);
            ret = AVI_write_frame(mOwner->avi_fd, (char*)(addr), 614400);

        好了,从共享内存指针const sp<IMemory> &data,一步步转取到实际的帧数据的共享内存句柄的过程,到现在就已经将完了。我们接着来讲我们的avi.

        今天周六,一大早去农批市场给娃买了水果回来,接着和大家分析下录制视频流程。昨天晚上说到,在Camerasource里read到的数据,是直接从hal层的processCaptureResult里返回上来的数据。如果在hal层的processCaptureResult里,出来的是没有经过任何转换的mjpeg数据,那么就可以用它来存avi文件了,但这是不可能的。这里出来的数据,已经是转成了适合在屏幕上显示的abgr数据了。

        如果我们不是录avi视频,而是录mp4视频的话,这里的数据,也是不能直接写mp4box,直接交给mpeg4writer去打包成mp4文件的。因为这里出来的数据,还需要转到mediacodec里去编码,然后才可以使用。这里顺带讲一下这个录制mp4的大致流程:

        CameraSource::read,取到hal层camera传上来的abgr数据后,会交给mediacodec去编码成h264的数据,可以从MediaCodecSource::Puller::onMessageReceived这个函数里的case kWhatPull:看到mSource->read(&mbuf);,这里的mSource就是CameraSource,这里从camerasource的read里取未编码的数据, 然后交由MediaCodecSource::feedEncoderInputBuffers函数。在这个函数里,会交数据交给mEncoder,而mEncoder就是在MediaCodecSource::initEncoder()里创建的mediacodec,见代码:

    //在status_t MediaCodecSource::initEncoder()函数里

    mEncoder = MediaCodec::CreateByType(
            mCodecLooper, outputMIME.c_str(), true /* encoder */);

    最后mediacodec编码成h264后,再由MPEG4Writer里的read,从MediaCodecSource::read里取到编码成了h264的数据,然后就可以直接按mp4规范来存的。

        好了,讲完了mp4的录制流程,我们再讲继续讲avi的录制流程。我们再来回顾一下我们录制avi视频的目的和要求,我们的目的是节省开销,不让录制视频时,要经历mjpg->I420->ABGR->h264->mp4这繁锁的流程,我们希望能将从usbcamera里出来的数据,一步到位的存到avi视频文件里,这中间可以节省三次编码转换的开销。

        我们的要求是,不能因为节省录制视频时的编码开销,而影响到了预览。因为预览要求的是abgr编码的数据,而存avi视频要的是mjpeg数据。同时,录制视频和预览,本质上用到的是同一处数据来源,它们是通过一个共享内存指针IMemory来传递的数据。如果我们在hal层,让从camera里出来的数据,不经过转换,就传上来,然后我们再从camerasource::read里取数据,直接调libavi接口写avi文件的话,写avi视频是可以成功的。但是不可避免的影响到了预览界面。而如果让hal层将视频数据转成了abgr格式后再传上来,预览是正常了,但又不能直接存为avi文件,还需要将abgr数据转回到mjpeg数据。这么一来,就失去了节省开销的意义。

        这么看来,似乎这是一个无解的问题?真的是这样么?当然不是,我们可以用空间来换时间。我先来说说空间换时间的思路,我们可以在hal层的camera里的processCaptureRequest里,将取到的数据,在做mjpg->I420->ABGR这个转换前,就copy一份出来。这份数据,就是多出来的这个空间。这份拷贝的真正的原始数据,单独用来存avi视频。原本的预览流程,不做任何改动。

       这样,我们的数据就分成了两路,一路是拷贝的未编码的原始数据。一路是原本的需要经过mjpg->I420->ABGR转换,然后交给预览使用的数据。

        这么一来,我们又面临了一个问题,即如何在hal层,将拷贝出来的原始数据,将给framework层的我们新增的AVIWriter去使用的问题。大家从我上面的代码里可以看得出,录制视频时,选择使用哪个打包器,都是在frameworks\av\media\libmediaplayerservice\StagefrightRecorder.cpp里的StagefrightRecorder::prepareInternal()函数里进行的。比如要录制mp4的视频,就会选择setupMPEG4orWEBMRecording()。同理,如果想要录制avi视频,就需要选择我们自己新增的setupAVIRecording()。 

        以usbcamera为例(普通的camera也是同一个道理同一个流程),我们编译出来的库文件为libusbcamera.so,而在setupAVIRecording()里调用到的AVIWriter,又放在libstagefright.so里。 虽然我们最终录制视频时,它们都会统一到mediaserver这个进程里来,但是毕竟是跨模块的,这块数据要怎么处理才能达到最好的性价比呢?

        也许有同学会说,为什么不直接在hal层里,将取出来的原始数据,写avi文件呢? 没错,这样做也可以,并且这样做是最省事的,不需要用空间来换时间开销,也不用考虑跨模块传数据的问题。但是这样就破坏了编程中的最重要的思想——模块化。并且,如果是在hal层里camera的processCaptureRequest函数里,直接存avi视频的话,还有一个很麻烦的问题。那就是,你怎么去将音频数据从framework层传下来?如果你仅仅是想录制没有声音的视频,那还好。如果你要同时录制声音,这里就又回到了刚刚的问题——怎么才能跨模块,将编码好的音频数据,传到hal层的camera的processCaptureRequest函数里。

        我们编程时,首先要想到的是模块化,便于封装接口,而不仅仅是为了图一时之快,而打破现有的系统录制视频流程,强行改变流程。这样的话,不说看着别扭,还会引起其他种种问题。

        好了,不卖关子了。我们要在现有的录制视频的这套流程里,插入录制avi视频的功能,同时不能影响到预览,就只能在hal层camera里的processCaptureRequest函数里拷一份数据出来,交给framework层的libstagefright.so里的AVIWriter使用。这个跨模块数据的传递,当然还是要使用共享内存了。

        别看共享内存看起来很复杂,但实际上很简单。我们可以在hal层camera里的openDevice里,通过ashmem_create_region来创建一块指定大小的内存,然后再通过mmap将它映射到本地的一个buff,然后我们就可以在processCaptureRequest里将取出来的帧数据,在进行libyuv::MJPGToI420->libyuv::I420ToABGR转换之前,就拷贝到这里的共享内存buff里。再然后,我们在AVIWriter里的read里,就直接从这一块buff里取数据就可以了。

        当然,这里又涉及到了一个问题,我们在hal层里camera里创建的共享内存,如何才能够被framework层的AVIWriter知道呢?要使用共享内存的关键,是要获得它的文件句柄。现在的问题是,怎么才能将这个句柄传过去。当然,想要传送文件句柄,如果是跨进程传递,会很麻烦。因为fd只是对本进程是有效、且唯一,进程A打开一个文件得到一个fd,不能直接为进程B使用,因为B中那个fd可能压根无效、或者对应其他文件。

        但是,大家要注意,我们的hal层的camera和framework层的aviwriter,看起来隔了很远。但实际上,在录制视频时,它们是在同一个进程mediaservice里的。所以在hal层的camera里通过ashmem_create_region创建的共享内存文件句柄fd,是可以直接交给AVIWriter里使用的。至于怎么传送这个fd,本人采用了一个非常简单直接粗暴的方法,那就是使用android的属性来设置。如:property_set("persist.avimemory.fd", fd); 然后在AVIWriter里,开始录相的时候,使用property_get("persist.avimemory.fd", sharedMemoryFd, "-1");,将这个文件句柄取出来,然后再mmap这个文件句柄,就可以将这块共享内存buff,映射到aviwriter里本地的buff指针了。

        是不是很简单?对,对于编程而言,很多事情看起来非常难,做的过程也确实非常难,但是一旦做完后,回过头来再看,就会发现相当的简单了。

        既然这里说到了共享内存在进程间文件句柄传输的问题,就顺便提一下,android的进程间传文件句柄的原理:

        原生Linux共享内存是通过传递已知的key来处理的,但是Android中不存在这种机制,Android是怎么处理的呢?那就是通过Binder传递文件描述符来处理,Android的Binder对于fd的传递也做了适配,原理其实就是在内核层为要传递的目标进程转换fd,因为在linux中fd只是对本进程是有效、且唯一,进程A打开一个文件得到一个fd,不能直接为进程B使用,因为B中那个fd可能压根无效、或者对应其他文件,不过,虽然同一个文件可以有多个文件描述符,但是文件只有一个,在内核层也只会对应一个inode节点与file对象,这也是内核层可以传递fd的基础,Binder驱动通过当前进程的fd找到对应的文件,然后为目标进程新建fd,并传递给目标进程,核心就是把进程A中的fd转化成进程B中的fd,看一下Android中binder的实现:

void binder_transaction(){
   ...
        case BINDER_TYPE_FD: {
        int target_fd;
        struct file *file;
        <!--关键点1 可以根据fd在当前进程获取到file ,多个进程打开同一文件,在内核中对应的file是一样-->
        file = fget(fp->handle);
        <!--关键点2,为目标进程获取空闲fd-->
        target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC);
        <!--关键点3将目标进程的空闲fd与file绑定-->
        task_fd_install(target_proc, target_fd, file);
        fp->handle = target_fd;
    } break;    
    ...
 }

<!--从当前进程打开的files中找到file在内核中的实例-->
struct file *fget(unsigned int fd)
{
    struct file *file;
    struct files_struct *files = current->files;
    rcu_read_lock();
    file = fcheck_files(files, fd);
    rcu_read_unlock();
    return file;
}


static void task_fd_install(
    struct binder_proc *proc, unsigned int fd, struct file *file)
{
    struct files_struct *files = proc->files;
    struct fdtable *fdt;
    if (files == NULL)
        return;
    spin_lock(&files->file_lock);
    fdt = files_fdtable(files);
    rcu_assign_pointer(fdt->fd[fd], file);
    spin_unlock(&files->file_lock);
}

        上面这段android进程间共享内存传文件句柄的描述是转自https://www.jianshu.com/p/d9bc9c668ba6,详细描述大伙可以去自行阅读。

        言归正传,现在取到文件句柄后,事情还没有做完,我们还有非常重要的一步没做,那就是hal层和framework层的共享内存数据的同步没有做控制。试想一下,hal层从camera里取出一帧数据,拷到共享内存buff后,AVIWriter里取这块buff,调用AVI_write_frame去写avi文件,正写了一半的时候,hal层的camera又取出了新的一帧数据,拷到了共享内存buff里,将AVIWriter里正在写的buff给覆盖了。

        这会导致什么后果?如果你的摄像头一直是静止的,对着某一处在拍摄,拍摄的也是静止的画面,画面里没有任何物体的移动,那么没有什么后果。因为上一帧和下一帧在图像上来看,都基本上没有什么区别,相似度非常的高。但是如果你的摄像头一直在运动,摄像头前面的影像也一直在运动,那么这就会导致你的画面下半屏会一直在闪。

        为什么说是屏幕的下半部份一直在闪,而不是上半屏呢?因为你正在写avi时,如果buff被覆盖了,那么覆盖了的也是后面还没来得及写完的数据,这时继续写的话,会将上一帧未写完的内容,替换成下一帧的内容,就会出现这种情况了。

        为了避免这种情况,我们必须引入一个同步机制。android本身有一套同步机制,专门针对GraphicBuffer的Fence。这个fence的大致工作流程是这样的,当camera填充好一块buff后,将它传到SurfaceFlinger等消费者时,会附带一个Fence标识,这个标识为非-1,然后SurfaceFlinge拿到这块buff后,不能马上就用,还需要检查下这个fence标识,如果为非-1,那么表示这块buff还在其他地方正在使用,它必须等它使用完,fence被置为-1时,才表示这块buff是没有人使用的了,你才可以取出来使用。

        这套机制,大家可以去网上搜着学习下,对理解android的camera和显示模块,有非常大的帮助。当然在我们的这套流程里,没有使用这个fence,但是我也参照了它的逻辑,设计了自己的一个数据结构。见代码:

	struct shared_frame {
	  int writering;
	  uint32_t length;
	  char frame_buff[YUV_640x480_LEN];
	};	

        然后在创建共享内存时,就创建这个数据结构大小的一块内存。见代码:

int mSharedMemoryFd = ashmem_create_region( "usbcamera_frame", sizeof(struct shared_frame) );

        这结构中的length,是指从usbcamera里出来来的实际的帧长度。frame_buff是预申请的一块640*480分辨率下,mjpeg数据能压缩到的最大的内存。这个大小是640*480*2,也就是说,mjpeg最多可以将原始数据压缩一半来存储。

        这里用到的同步标志就是writering,它的作用等同于fence.在hal层的camera里创建一块共享内存后,会先将它的值置为-1.然后在AVIWriter里,调用AVI_write_frame写avi数据前,将它置为1.写完后,将它置为0. 这样,我们就可以在hal层的camera里来通过这个标志判断,当writering为0时,才对这块共享内存进行写数据操作,否则的话,就直接跳过这一帧不做处理。见代码:

//writering==1时,表示AVIWriter正在写操作,这时不能往共享内存里写新的数据。
//这一帧直接跳过不管,继续接收新的帧,直到AVIWriter里没有对共享内存进行读
//操作,这时writering为0,才继续往共享内存里写新数据
if((mSharedMemoryFd != -1) && (m_SharedMemoryBuffer->writering == 0) )
{
	m_SharedMemoryBuffer->length = frame->bytesused;
	
	mMod = frame->bytesused % 8;
	if(mMod)
	{
		//以8个字节,也就是uint64_t对齐。理论上讲,这样操作,比C原生的memcpy一次按一个字节来拷,要快
		//7/8的时间。如果memcpy要80微秒,mymemcpy只需要10微秒。但实际上没有省下这么多时间,大概能省一
		//半以上的时间。。 不能在这里耽误时间,否则会引起acquireFence->wait报错。
		mymemcpy(m_SharedMemoryBuffer->frame_buff, frame->buf, (frame->bytesused+mMod)/8 + (8-mMod));
	}
	else
	{
		mymemcpy(m_SharedMemoryBuffer->frame_buff, frame->buf, (frame->bytesused)/8);
	}
	
   // memcpy(m_SharedMemoryBuffer->frame_buff, frame->buf, frame->bytesused);
}

        这样,就可以做到完美的同步了。大家应该看到了上面的拷贝内存函数用的不是系统的memcpy,而是我自己写的一个拷贝函数。这样做的目的,是为了节省时间开销。

        camera取两帧数据间的时间间隔非常的短,大概是几十毫秒的样子。如果我们在这个间隔中间耽误的时间久了,就会引起超时报错。这个超时报错,是在processCaptureRequest函数里取到帧数据后,转码之前处理的。见代码:

        sp<Fence> acquireFence = new Fence(srcBuf.acquire_fence);
        e = acquireFence->wait(1000); /* FIXME: magic number */

        如果在一定的时间内没有处理完,就会报错,见代码:

        if(e != NO_ERROR) {
            do GraphicBufferMapper::get().unlock(*request->output_buffers[i].buffer); while(i--);
            return NO_INIT;
        }

        虽然memcpy拷贝时间不会太长,但是如果是在极端情况下,也就是取出来的一帧数据是640*480*2=614400的情况下,如果是按字节char来拷贝,大概需要1毫秒左右的时间,甚至更多。我们为了节省这个时间开销,避免引起堵塞,要尽一切可能降低这个开销。mymemcpy的代码如下:

void * Camera::mymemcpy ( void * dst,const void * src,size_t count)
{
	void * ret = dst;
	while (count--) {
		*(uint64_t *)dst = *(uint64_t *)src;
		dst = (uint64_t *)dst + 1;
		src = (uint64_t *)src + 1;
	}
	return(ret);
}

        好了,在系统里新增录制avi视频的功能,到这里就基本上讲完了。还剩下最后一点录制avi视频时,音视频同步的问题没讲。 这个逻辑不复杂,但是调起来,参数没设对的话,也是挺头疼的。如果大家有兴趣的话,我另开一篇博客再讲讲。

        至于这篇博客里说的新增录制avi视频功能的全套完整代码,有需要的同学,可以给我留言。好了,老婆催着吃饭了,先不写了。欢迎大家留言交流。

猜你喜欢

转载自blog.csdn.net/xuhui_7810/article/details/95677253