上一篇文章我們講到圖片轉字符畫,這篇文章要實現 視頻 轉 字符畫效果。
我們看一下實現出來的效果圖:
效果圖有點糊,原文的效果圖會更好
實現的效果還是讓人挺滿意的。我們下面說一下具體的實現步驟,
我們分開一步一步的講:
視頻取幀視頻取幀的整個功能最麻煩的一步,目前Android視頻取幀的方法有好幾種。其中有使用SDK自帶的MediaMetadataRetriever直接獲取bimap的,但是缺點就是慢。
也有使用強大的FFmpeg庫的,但是需要針對編譯不同架構的CPU編譯不同的so文件十分的麻煩。
也有人推薦使用一個名為Jcodec的庫,開發效率上來說這個工具確實十分的好,但是運行起來真的十分的慢,我寫了個Demo取一幀大概要我4s的時間(測試手機是Redmi note 7 pro),所以只用他的視頻合成功能(雖然仍然很慢具體的解決辦法還沒找到)。
後來在別的大佬博客裡面找到一篇使用原生接口MediaCodec硬解碼視頻的文章,用該方法取幀完美解決對不同機型的兼容性問題,因為使用的原生接口速度也是可以保證的。
但主要的問題點是 MediaCodec 解碼返回的幀圖片數據是YUV格式的,它跟我們平時使用的 RGB 格式很不一樣的是它的三個值表示的是亮度,色度,飽和度。
YUV下也分不同的格式分別有:Y'UV, YUV, YCbCr,YPbPr等,安卓設備因為 API 21 統一的原因都能使用 COLOR_FormatYUV420Flexible 格式,使得 MediaCodec 的所有硬體解碼都支持這種格式。
但這樣解碼後得到的 YUV420 的具體格式又會因設備而異,有:YUV420Planar,YUV420SemiPlanar,YUV420PackedSemiPlanar 等,我們可以使用 Image 類來處理這些格式統一處理向 NV21 進行轉換。
然後我們可以對 Image 類進行轉換成 Bitmap,再對 Bimap 的進行像素轉換成字符數組再繪製成圖片保存作為轉換字符畫視頻 的其中一幀。
具體實現,首先我們在解碼的過程的中需要獲取設備是否支持 COLOR_FormatYUV420Flexible 幀格式,然後初始化幾個重要的對象:
...
MediaExtractor extractor = null;
MediaFormat mediaFormat = null;
MediaCodec decoder = null;
extractor = initMediaExtractor(file);//使用視頻文件對象初始化extractor
mediaFormat = initMediaFormat(videoPath, extractor);
decoder = initMediaCodec(mediaFormat);
//初始化解碼配置
decoder.configure(mediaFormat, null, null, 0);
decoder.start();
//開始解碼
...
static private MediaExtractor initMediaExtractor(File path) throws IOException {
MediaExtractor extractor = null;
extractor = new MediaExtractor();
extractor.setDataSource(path.toString());
return extractor;
}
static private MediaFormat initMediaFormat(String path, MediaExtractor extractor) {
//選擇解碼通道
int trackIndex = selectTrack(extractor);
if (trackIndex < 0) {
throw new RuntimeException("No video track found in " + path);
}
extractor.selectTrack(trackIndex);
MediaFormat mediaFormat = extractor.getTrackFormat(trackIndex);
return mediaFormat;
}
static public MediaCodec initMediaCodec(MediaFormat mediaFormat) throws IOException {
MediaCodec decoder = null;
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
decoder = MediaCodec.createDecoderByType(mime);
//showSupportedColorFormat(decoder.getCodecInfo().getCapabilitiesForType(mime));
if (isColorFormatSupported(decodeColorFormat, decoder.getCodecInfo().getCapabilitiesForType(mime))) {
// 設置 解碼格式
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, decodeColorFormat);
} else {
}
return decoder;
}初始化完成後我們可以利用這三個對象進行關鍵幀獲取:
private static Bitmap getBitmapBySec(MediaExtractor extractor, MediaFormat mediaFormat, MediaCodec decoder, long sec) throws IOException {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
Bitmap bitmap = null;
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
boolean stopDecode = false;
final int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
final int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
long presentationTimeUs = -1;
int outputBufferId;
Image image = null;
//視頻定位到指定的時間的上一幀
extractor.seekTo(sec, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
//因為extractor定位的幀不是準確的,所以我們要用一個循環不停讀取下一幀來獲取我們想要的時間畫面。
while (!sawOutputEOS && !stopDecode) {
if (!sawInputEOS) {
int inputBufferId = decoder.dequeueInputBuffer(-1);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = decoder.getInputBuffer(inputBufferId);
int sampleSize = extractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {
decoder.queueInputBuffer(inputBufferId, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
sawInputEOS = true;
} else {
//獲取定位的幀的時間
presentationTimeUs = extractor.getSampleTime();
//把定位的幀壓入隊列
decoder.queueInputBuffer(inputBufferId, 0, sampleSize, presentationTimeUs, 0);
//跳到下一幀
extractor.advance();
}
}
}
outputBufferId = decoder.dequeueOutputBuffer(info, DEFAULT_TIMEOUT_US);
if (outputBufferId >= 0) {
//能夠有效輸出
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0 | presentationTimeUs >= sec) {
//時間是指定時間或者已經是視頻結束時間,停止循環
sawOutputEOS = true;
boolean doRender = (info.size != 0);
if (doRender) {
//獲取指定時間解碼出來的Image對象。
image = decoder.getOutputImage(outputBufferId);
//將Image轉換成Bimap
stream.close();
image.close();
}
}
decoder.releaseOutputBuffer(outputBufferId, true);
}
}
return bitmap;
}獲取到了幀畫面數據,下面我們可以做關於 Image 對 Bimap 的轉換,主要是用到 YuvImage 這個類,在使用 YuvImage 這個類前需要把 YUV_420_888 的編碼格式轉成 NV21 格式:
...
image = decoder.getOutputImage(outputBufferId);
YuvImage yuvImage = new YuvImage(YUV_420_888toNV21(image), ImageFormat.NV21, width, height, null);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, stream);
bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
...
private static byte[] YUV_420_888toNV21(Image image) {
Rect crop = image.getCropRect();
int format = image.getFormat();
int width = crop.width();
int height = crop.height();
Image.Plane[] planes = image.getPlanes();
byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
byte[] rowData = new byte[planes[0].getRowStride()];
//if (VERBOSE) Log.v("YUV_420_888toNV21", "get data from " + planes.length + " planes");
int channelOffset = 0;
int outputStride = 1;
for (int i = 0; i < planes.length; i++) {
switch (i) {
case 0:
channelOffset = 0;
outputStride = 1;
break;
case 1:
channelOffset = width * height + 1;
outputStride = 2;
break;
case 2:
channelOffset = width * height;
outputStride = 2;
break;
}
ByteBuffer buffer = planes[i].getBuffer();
int rowStride = planes[i].getRowStride();
int pixelStride = planes[i].getPixelStride();
int shift = (i == 0) ? 0 : 1;
int w = width >> shift;
int h = height >> shift;
buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
for (int row = 0; row < h; row++) {
int length;
if (pixelStride == 1 && outputStride == 1) {
length = w;
buffer.get(data, channelOffset, length);
channelOffset += length;
} else {
length = (w - 1) * pixelStride + 1;
buffer.get(rowData, 0, length);
for (int col = 0; col < w; col++) {
data[channelOffset] = rowData[col * pixelStride];
channelOffset += outputStride;
}
}
if (row < h - 1) {
buffer.position(buffer.position() + rowStride - length);
}
}
// if (VERBOSE) Log.v("", "Finished reading data from plane " + i);
}
return data;
}這樣我們就能獲取到了幀圖片的 Bitmap 數據了,剩下的步驟都跟上一篇文章的圖片轉換差不多,當我們所有的幀都轉換完以後,我們就可以把這些圖片按順序合成視頻了,這裡我調用的是上面提到的 Jcodec 這個工具,它有支持圖片合成視頻的功能,代碼如下:
static public String convertVideoBySourcePics(Context context, String picsDri) {
SeekableByteChannel out = null;
//找到輸出目錄,沒有的話創建
File destDir = new File(Environment.getExternalStorageDirectory() + "/FunVideo_Video");
if (!destDir.exists()) {
destDir.mkdirs();
}
//新建輸出文件
File file = new File(destDir.getPath() + "/funvideo_" + System.currentTimeMillis() + ".mp4");
try {
file.createNewFile();
// for Android use: AndroidSequenceEncoder
File _piscDri = new File(picsDri);
//創建編碼對象
AndroidSequenceEncoder encoder = AndroidSequenceEncoder.createSequenceEncoder(file, 5);
for (File childFile : _piscDri.listFiles()) {
Bitmap bitmap = BitmapUtils.getBitmapByUri(context, Uri.fromFile(childFile));
encoder.encodeImage(bitmap);
bitmap.recycle();
}
//結束編碼
encoder.finish();
//通知系統添加了視頻文件。
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri contentUri = Uri.fromFile(file);
mediaScanIntent.setData(contentUri);
context.sendBroadcast(mediaScanIntent);
//Log.i("addGraphToGallery", "ok");
} catch (IOException ex) {
ex.printStackTrace();
} finally {
NIOUtils.closeQuietly(out);
}
//轉化完成輸出文件路徑
return file.getPath();
}調用 Jcodec 的轉換如果視頻在 15s 以內轉換的效率還是可以的,大於 15s 的視頻轉換就會變得十分的慢,可能是我自己的原因也可能是這個工具本來也存在一些優化的問題。
鑑於上面的視頻解碼取幀,最好的視頻編碼合成當然也是用原生的 MediaMetadataRetriever 來做。
思路大概跟上面的方法反著來,看著是不是很清晰了,具體實現方法我就不細說了,因為我也還沒做,後面會基於這個思路來優化合成視頻這一模塊。
字符畫轉換的全部內容大概都到這裡了,謝謝大家閱讀,喜歡的話可以給個贊。
完整項目源碼地址:
https://github.com/452kinton/CharacterDance作者:Kinton
來源:https://www.jianshu.com/p/16ef3bf9ac5c