I'm looking for efficient way to reduce some video weight (as a File
, for upload) and obvious answer for that is: lets reduce resolution! (fullHD or 4K not needed, simple HD is sufficient for me) I've tried lot of ways which should work through lot of APIs (needed 10) and best way was using android-ffmpeg-java, BUT... on my pretty fast almost-current flagship device whole process lasts about length_of_video*4 seconds and also this lib weight is 9 Mb, this amount increases my app size... No wai! (12 Mb to 1 Mb is nice result, but still too many flaws)
So I've decided to use native Android ways to do this, MediaMuxer
and MediaCodec
- they are available from API18 and API16 respectivelly (older devices users: sorry; but they also often have "lower-res" camera). Below method almost works - MediaMuxer
do NOT respect MediaFormat.KEY_WIDTH
and MediaFormat.KEY_HEIGHT
- extracted File
is "re-compressed", weight is a bit smaller, but resolution is the same as in original video File
...
So, question: How to compress and re-scale/change resolution of video using MediaMuxer
and other accompanying classes and methods?
public File getCompressedFile(String videoPath) throws IOException{
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(videoPath);
int trackCount = extractor.getTrackCount();
String filePath = videoPath.substring(0, videoPath.lastIndexOf(File.separator));
String[] splitByDot = videoPath.split("\\.");
String ext="";
if(splitByDot!=null && splitByDot.length>1)
ext = splitByDot[splitByDot.length-1];
String fileName = videoPath.substring(videoPath.lastIndexOf(File.separator)+1,
videoPath.length());
if(ext.length()>0)
fileName=fileName.replace("."+ext, "_out."+ext);
else
fileName=fileName.concat("_out");
final File outFile = new File(filePath, fileName);
if(!outFile.exists())
outFile.createNewFile();
MediaMuxer muxer = new MediaMuxer(outFile.getAbsolutePath(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
HashMap<Integer, Integer> indexMap = new HashMap<Integer, Integer>(trackCount);
for (int i = 0; i < trackCount; i++) {
extractor.selectTrack(i);
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if(mime!=null && mime.startsWith("video")){
int currWidth = format.getInteger(MediaFormat.KEY_WIDTH);
int currHeight = format.getInteger(MediaFormat.KEY_HEIGHT);
format.setInteger(MediaFormat.KEY_WIDTH, currWidth>currHeight ? 960 : 540);
format.setInteger(MediaFormat.KEY_HEIGHT, currWidth>currHeight ? 540 : 960);
//API19 MediaFormat.KEY_MAX_WIDTH and KEY_MAX_HEIGHT
format.setInteger("max-width", format.getInteger(MediaFormat.KEY_WIDTH));
format.setInteger("max-height", format.getInteger(MediaFormat.KEY_HEIGHT));
}
int dstIndex = muxer.addTrack(format);
indexMap.put(i, dstIndex);
}
boolean sawEOS = false;
int bufferSize = 256 * 1024;
int offset = 100;
ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
muxer.start();
while (!sawEOS) {
bufferInfo.offset = offset;
bufferInfo.size = extractor.readSampleData(dstBuf, offset);
if (bufferInfo.size < 0) {
sawEOS = true;
bufferInfo.size = 0;
} else {
bufferInfo.presentationTimeUs = extractor.getSampleTime();
bufferInfo.flags = extractor.getSampleFlags();
int trackIndex = extractor.getSampleTrackIndex();
muxer.writeSampleData(indexMap.get(trackIndex), dstBuf,
bufferInfo);
extractor.advance();
}
}
muxer.stop();
muxer.release();
return outFile;
}
PS. lot of usefull stuff about muxer here, above code bases on MediaMuxerTest.java
, method cloneMediaUsingMuxer
You can try Intel INDE Media for Mobile, tutorials on https://software.intel.com/en-us/articles/intel-inde-media-pack-for-android-tutorials. It has a sample that shows how to use it to transcode=recompress video files.
You can set smaller resolution and\or bitrate to output to get smaller file https://github.com/INDExOS/media-for-mobile/blob/master/Android/samples/apps/src/com/intel/inde/mp/samples/ComposerTranscodeCoreActivity.java
The MediaMuxer is not involved in the compression or scaling of video. All it does is take the H.264 output from MediaCodec and wrap it in a .mp4 file wrapper.
Looking at your code, you're extracting NAL units with MediaExtractor and immediately re-wrapping them with MediaMuxer. This should be extremely fast and have no impact on the video itself, as you're just re-wrapping the H.264.
To scale the video you need to decode the video with a MediaCodec decoder, feeding the NAL units from MediaExtractor into it, and re-encode it with a MediaCodec encoder, passing the frames to a MediaMuxer.
You've found bigflake.com; see also Grafika. Neither of these has exactly what you're looking for, but the various pieces are there.
It's best to decode to a Surface, not a ByteBuffer. This requires API 18, but for sanity it's best to forget that MediaCodec existed before then. And you'll need API 18 for MediaMuxer anyway.
Basing on bigflake.com/mediacodec/ (awesome source of knowledge about Media-classes) I've tried few ways and finally ExtractDecodeEditEncodeMuxTest turned out very helpfull. This test wasn't described in article on bigflake site, but it can be found HERE next to other classes mentioned in text.
So, I've copied most of code from above mentioned
ExtractDecodeEditEncodeMuxTest
class and there it is:VideoResolutionChanger
. It gives me 2Mb HD video from 16 Mb fullHD. Nice! And fast! On my device whole process is a bit longer than input video duration, e.g. 10 secs video input -> 11-12 secs of processing. Withffmpeg-java
it would be smth about 40 secs or more (and 9 Mb more for app).Here we go:
VideoResolutionChanger:
it needs also
InputSurface
,OutputSurface
andTextureRender
, which are placed next toExtractDecodeEditEncodeMuxTest
(above HERE link). Put these three in same package withVideoResolutionChanger
and use it like this:Where
videoFilePath
might be obtained fromFile
usingfile.getAbsolutePath()
.I know it's not the cleanest and probably not most-effective/efficient way, but I've been looking for similar code for last two days and found lot of topics, which most redirected me to INDE, ffmpeg or jcodec, other were left without proper answer. So I'm leaving it here, use this wisely!
LIMITATIONS:
Activity
. best way is to createIntentService
and pass input file pathString
inIntent
s extraBundle
. Then you can runchangeResolution
stright insideonHandleIntent
;MediaMuxer
introduced);WRITE_EXTERNAL_STORAGE
, API19 and above has this "built-in";@fadden THANK YOU for your work and support! :)