微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如何在 android 上录制、加密在内存中和多路复用音频和视频而不会使文件不同步?

如何解决如何在 android 上录制、加密在内存中和多路复用音频和视频而不会使文件不同步?

我们正在尝试将 Android 设备中的视频和音频保存到加密文件中。我们当前的实现通过 MediaEncoder 类将麦克风和摄像头的输出进行管道传输。由于数据是从 MediaEncoder 输出的,我们正在加密并将字节缓冲区的内容写入磁盘。然而,这种方法有效,当尝试使用 FFMPEG 将文件拼接在一起时,我们注意到两个流似乎在流中间的某个地方不同步。这种方法似乎丢失了许多重要的元数据,特别是演示时间戳和帧速率数据,因为 ffmpeg 必须做一些猜测工作来混合文件

是否有技术可以在不使用 Mediamuxer 的情况下保持这些流同步?视频采用 H.264 编码,音频采用 AAC 编码。

其他方法: 我们尝试使用 Mediamuxer输出数据多路复用到文件,但我们的用例要求我们在将数据字节保存到磁盘之前对其进行加密,这消除了使用认构造函数的可能性。

此外,我们尝试使用新添加的 (API 26) 构造函数,该构造函数采用 FileDescriptor 并指向包装加密文档 (https://android.googlesource.com/platform/development/+/master/samples/Vault/src/com/example/android/vault/EncryptedDocument.java) 的 ParcelFileDescriptor。但是,这种方法会导致本机层崩溃,我们认为这可能与源代码 (https://android.googlesource.com/platform/frameworks/base.git/+/master/media/java/android/media/MediaMuxer.java#353) 中有关本机编写器试图对输出文件进行内存映射的注释有关。

import android.graphics.YuvImage
import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.media.Mediamuxer
import com.callyo.video_10_21.Utils.YuvImageUtils.convertNV21toYUV420Planar
import java.io.FileDescriptor
import java.util.*
import java.util.concurrent.atomic.atomicreference
import kotlin.properties.Delegates

class VideoEncoderProcessor(
   private val fileDescriptor: FileDescriptor,private val width: Int,private val height: Int,private val frameRate: Int
): MediaCodec.Callback() {
   private lateinit var videoFormat: MediaFormat
   private var trackIndex by Delegates.notNull<Int>()
   private var mediamuxer: Mediamuxer
   private val mediaCodec = createEncoder()
   private val pendingVideoEncoderInputBufferIndices = atomicreference<LinkedList<Int>>(LinkedList())

   companion object {
       private const val VIDEO_FORMAT = "video/avc"
   }

  init {
       mediamuxer = Mediamuxer(fileDescriptor,Mediamuxer.OutputFormat.muxer_OUTPUT_MPEG_4)
       mediaCodec.setCallback(this)
       mediaCodec.start()
   }

   private fun createEncoder(): MediaCodec {
       videoFormat = MediaFormat.createVideoFormat(VIDEO_FORMAT,width,height).apply {
           setInteger(MediaFormat.KEY_FRAME_RATE,frameRate)
           setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
           setInteger(MediaFormat.KEY_BIT_RATE,width * height * 5)
           setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,1)
       }

       return MediaCodec.createEncoderByType(VIDEO_FORMAT).apply {
           configure(videoFormat,null,MediaCodec.CONfigURE_FLAG_ENCODE)
       }
   }

   override fun onInputBufferAvailable(codec: MediaCodec,index: Int) {
       // logic for handling stream end omitted for clarity

       /* Video frames come in asynchronously from input buffer availability
        * so we need to keep track of available buffers in queue */
       pendingVideoEncoderInputBufferIndices.get().add(index)
   }

   override fun onError(codec: MediaCodec,e: MediaCodec.CodecException) {}

   override fun onOutputFormatChanged(codec: MediaCodec,format: MediaFormat) {
       trackIndex = mediamuxer.addTrack(format)
       mediamuxer.start()
   }

   override fun onOutputBufferAvailable(codec: MediaCodec,index: Int,bufferInfo: MediaCodec.BufferInfo) {
       val buffer = mediaCodec.getoutputBuffer(index)
       buffer?.apply {
           if (bufferInfo.size != 0) {
               limit(bufferInfo.offset + bufferInfo.size)
               rewind()
               mediamuxer.writeSampleData(trackIndex,this,bufferInfo)
           }
       }

       mediaCodec.releaSEOutputBuffer(index,false)

       if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
           mediaCodec.stop()
           mediaCodec.release()
           mediamuxer.stop()
           mediamuxer.release()
       }
   }

   // Public method that receives raw unencoded video data
   fun encode(yuvImage: YuvImage) {
       // logic for handling stream end omitted for clarity

       pendingVideoEncoderInputBufferIndices.get().poll()?.let { index ->
           val buffer = mediaCodec.getInputBuffer(index)
           buffer?.clear()
           // converting frame to correct color format
           val input =
                   yuvImage.convertNV21toYUV420Planar(ByteArray(yuvImage.yuvData.size),yuvImage.width,yuvImage.height)
           buffer?.put(input)
           buffer?.let {
               mediaCodec.queueInputBuffer(index,input.size,System.nanoTime() / 1000,0)
           }
       }
   }
}



附加信息: 我使用 MediaCodec.Callback() (https://developer.android.com/reference/kotlin/android/media/MediaCodec.Callback?hl=en) 异步处理编码。

解决方法

简介

我将参考以下问答:sync audio and video with mediacodec and mediamuxer

由于信息丢失:

为了同步音频和视频,您必须“计算每帧视频应播放的音频样本数”

作者继续并提供了一个例子,例如

这取决于采样率和帧率:

在 24fps 和 48000Hz 时每帧很长 (48000hz/24fps)= 2000 个样本

在 25 fps 和 48000Hz 时:(48000hz/25fps)= 1920 个样本

示例

看看下面的例子,它混合了一个视频和音频文件,在那里他们设置了样本大小并组合了视频和音频(来自:https://github.com/Docile-Alligator/Infinity-For-Reddit/blob/61c5682b06fb3739a9f980700e6602ae0f39d5a2/app/src/main/java/ml/docilealligator/infinityforreddit/services/DownloadRedditVideoService.java#L506

private boolean muxVideoAndAudio(String videoFilePath,String audioFilePath,String outputFilePath) {
    try {
        File file = new File(outputFilePath);
        file.createNewFile();
        MediaExtractor videoExtractor = new MediaExtractor();
        videoExtractor.setDataSource(videoFilePath);
        MediaExtractor audioExtractor = new MediaExtractor();
        audioExtractor.setDataSource(audioFilePath);
        MediaMuxer muxer = new MediaMuxer(outputFilePath,MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

        videoExtractor.selectTrack(0);
        MediaFormat videoFormat = videoExtractor.getTrackFormat(0);
        int videoTrack = muxer.addTrack(videoFormat);

        audioExtractor.selectTrack(0);
        MediaFormat audioFormat = audioExtractor.getTrackFormat(0);
        int audioTrack = muxer.addTrack(audioFormat);
        boolean sawEOS = false;
        int offset = 100;
        int sampleSize = 2048 * 1024;
        ByteBuffer videoBuf = ByteBuffer.allocate(sampleSize);
        ByteBuffer audioBuf = ByteBuffer.allocate(sampleSize);
        MediaCodec.BufferInfo videoBufferInfo = new MediaCodec.BufferInfo();
        MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();

        videoExtractor.seekTo(0,MediaExtractor.SEEK_TO_CLOSEST_SYNC);
        audioExtractor.seekTo(0,MediaExtractor.SEEK_TO_CLOSEST_SYNC);

        muxer.start();

        while (!sawEOS) {
            videoBufferInfo.offset = offset;
            videoBufferInfo.size = videoExtractor.readSampleData(videoBuf,offset);

            if (videoBufferInfo.size < 0 || audioBufferInfo.size < 0) {
                sawEOS = true;
                videoBufferInfo.size = 0;
            } else {
                videoBufferInfo.presentationTimeUs = videoExtractor.getSampleTime();
                videoBufferInfo.flags = videoExtractor.getSampleFlags();
                muxer.writeSampleData(videoTrack,videoBuf,videoBufferInfo);
                videoExtractor.advance();
            }
        }

        boolean sawEOS2 = false;
        while (!sawEOS2) {
            audioBufferInfo.offset = offset;
            audioBufferInfo.size = audioExtractor.readSampleData(audioBuf,offset);

            if (videoBufferInfo.size < 0 || audioBufferInfo.size < 0) {
                sawEOS2 = true;
                audioBufferInfo.size = 0;
            } else {
                audioBufferInfo.presentationTimeUs = audioExtractor.getSampleTime();
                audioBufferInfo.flags = audioExtractor.getSampleFlags();
                muxer.writeSampleData(audioTrack,audioBuf,audioBufferInfo);
                audioExtractor.advance();

            }
        }

        try {
            muxer.stop();
            muxer.release();
        } catch (IllegalStateException ignore) {}
    } catch (IOException e) {
        e.printStackTrace();
        return false;
    }

    return true;
}

看看@以下页面:https://sisik.eu/blog/android/media/mix-audio-into-video

从那里开始,他们在以下部分提供了一个很好的示例:Muxing Frames Into MP4 With MediaMuxer,您可以使用它将文件重新拼接在一起。

从那里:

就我而言,我想从 MPEG-4 视频和 AAC/M4A 音频文件中获取输入,> 并将这两个输入复用到一个 MPEG-4 输出视频文件中。为了实现这一点,我 创建了以下 mux() 方法

fun mux(audioFile: String,videoFile: String,outFile: String) {

    // Init extractors which will get encoded frames
    val videoExtractor = MediaExtractor()
    videoExtractor.setDataSource(videoFile)
    videoExtractor.selectTrack(0) // Assuming only one track per file. Adjust code if this is not the case.
    val videoFormat = videoExtractor.getTrackFormat(0)

    val audioExtractor = MediaExtractor()
    audioExtractor.setDataSource(audioFile)
    audioExtractor.selectTrack(0) // Assuming only one track per file. Adjust code if this is not the case.
    val audioFormat = audioExtractor.getTrackFormat(0)

    // Init muxer
    val muxer = MediaMuxer(outFile,MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
    val videoIndex = muxer.addTrack(videoFormat)
    val audioIndex = muxer.addTrack(audioFormat)
    muxer.start()

    // Prepare buffer for copying
    val maxChunkSize = 1024 * 1024
    val buffer = ByteBuffer.allocate(maxChunkSize)
    val bufferInfo = MediaCodec.BufferInfo()

    // Copy Video
    while (true) {
        val chunkSize = videoExtractor.readSampleData(buffer,0)

        if (chunkSize > 0) {
            bufferInfo.presentationTimeUs = videoExtractor.sampleTime
            bufferInfo.flags = videoExtractor.sampleFlags
            bufferInfo.size = chunkSize

            muxer.writeSampleData(videoIndex,buffer,bufferInfo)

            videoExtractor.advance()

        } else {
            break
        }
    }

    // Copy audio
    while (true) {
        val chunkSize = audioExtractor.readSampleData(buffer,0)

        if (chunkSize >= 0) {
            bufferInfo.presentationTimeUs = audioExtractor.sampleTime
            bufferInfo.flags = audioExtractor.sampleFlags
            bufferInfo.size = chunkSize

            muxer.writeSampleData(audioIndex,bufferInfo)
            audioExtractor.advance()
        } else {
            break
        }
    }

    // Cleanup
    muxer.stop()
    muxer.release()

    videoExtractor.release()
    audioExtractor.release()
}

更新

根据您的评论,我认为主要问题是 fileDescriptor。具体来说,他们只将 RandomAccessFile 用于文件描述符,但本机接口是进行读取的接口。

我有一个建议,也许您应该考虑使用 FileDescriptor 而不是基于文件的 in-memory

因此,读取加密文件并在内存中对其进行解密,然后将这些字节转换为新的内存中的 fileDescriptor。将内存中的 fileDescriptor 提供给 muxor,看看会发生什么。

有一个很好的答案,他们使用安全的私有仅应用套接字来创建文件描述符,请参阅:Create an in-memory FileDescriptor

专门检查该答案的第二部分,从:

一个更好但更复杂的解决方案是在 文件系统命名空间。 参考:https://stackoverflow.com/a/62651005/1688441

所以更详细:

  1. 读取加密文件并将其解密为字节但保留在内存中
  2. 在您应用的私有数据区域中创建一个 localSocket 和一个服务器。
  3. 开始侦听您的服务器并接受未加密的字节。
  4. 创建一个 localSocket 客户端并将未加密的字节泵送到服务器。
  5. 还将客户端的 fileDescriptor 传递给 muxor。

正如答案所述:

这确实在文件系统上创建了一个文件,但是通过套接字的任何数据都不会写入磁盘,它完全是 在记忆中。文件只是一个代表socket的名字,类似 /dev 中代表设备的文件。因为插座是 通过文件系统访问,它受制于通常的文件系统 权限,因此很容易通过放置来限制对套接字的访问 应用程序私有数据区中的套接字。

由于这种技术在文件系统上创建了一个文件,所以它将是一个 完成后删除文件的好主意,也许还可以 每隔一段时间检查并清理旧的套接字,以防您的应用程序 崩溃并留下旧文件。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。