如何解决如何在 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
所以更详细:
- 读取加密文件并将其解密为字节但保留在内存中
- 在您应用的私有数据区域中创建一个 localSocket 和一个服务器。
- 开始侦听您的服务器并接受未加密的字节。
- 创建一个 localSocket 客户端并将未加密的字节泵送到服务器。
- 还将客户端的 fileDescriptor 传递给 muxor。
正如答案所述:
这确实在文件系统上创建了一个文件,但是通过套接字的任何数据都不会写入磁盘,它完全是 在记忆中。文件只是一个代表socket的名字,类似 /dev 中代表设备的文件。因为插座是 通过文件系统访问,它受制于通常的文件系统 权限,因此很容易通过放置来限制对套接字的访问 应用程序私有数据区中的套接字。
由于这种技术在文件系统上创建了一个文件,所以它将是一个 完成后删除文件的好主意,也许还可以 每隔一段时间检查并清理旧的套接字,以防您的应用程序 崩溃并留下旧文件。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。