diff --git a/app/build.gradle b/app/build.gradle index 60b4b0fe..7936d3d0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,6 +44,8 @@ dependencies { compile 'commons-io:commons-io:2.5' compile group: 'com.google.guava', name: 'guava', version: '20.0' compile 'com.github.codekidX:storage-chooser:2.0.3' + implementation group: 'org.javatuples', name: 'javatuples', version: '1.2' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.8.11.1' testCompile 'junit:junit:4.12' testCompile "org.robolectric:robolectric:3.6.1" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1f0356fa..f573001f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,10 @@ + + + + resultHandler) { @@ -106,6 +146,27 @@ public void onFailure(String message) call(command, handler); } + public static void probeGetOutput(@NonNull final String [] command, + @NonNull final ResultCallbackHandler resultHandler) + { + ExecuteBinaryResponseHandler handler = new ExecuteBinaryResponseHandler() + { + @Override + public void onSuccess(String message) + { + resultHandler.onResult(message); + } + + @Override + public void onFailure(String message) + { + resultHandler.onResult(message); + } + }; + + probe(command, handler); + } + /** * Cancel the current in-progress invocation of FFmpeg */ @@ -172,244 +233,284 @@ public static Long timestampToMs(String timestamp) public static void getMediaDetails(final File mediaFile, final ResultCallbackHandler resultHandler) { - if(ffmpeg == null || ffmpeg.isCommandRunning()) - { - Log.d(TAG, "Failed to get media details, FFmpeg " + - (ffmpeg == null ? "is not initialized" : "is already running")); - resultHandler.onResult(null); - return; - } + String [] command = {"-i", mediaFile.getAbsolutePath(), + "-v", "quiet", "-print_format", "json", "-show_streams", "-show_format"}; - String [] command = {"-i", mediaFile.getAbsolutePath()}; - callGetOutput(command, new ResultCallbackHandler() + probeGetOutput(command, new ResultCallbackHandler() { @Override - public void onResult(String mediaDetailsStr) + public void onResult(String mediaDetailsJsonStr) { - Log.d(TAG, "Media details on " + mediaFile.getAbsolutePath() + "\n"); - for(String line : mediaDetailsStr.split("\n")) - { - Log.d(TAG, line); - } - MediaInfo info = parseMediaInfo(mediaFile, mediaDetailsStr); + MediaInfo info = parseMediaInfo(mediaFile, mediaDetailsJsonStr); resultHandler.onResult(info); } }); } - static MediaInfo parseMediaInfo(File mediaFile, String string) + static MediaInfo parseMediaInfo(File mediaFile, String mediaDetailsJsonStr) { long durationMs = 0; - Integer totalBitrate = null; + Integer totalBitrateK = null; MediaContainer container = null; VideoCodec videoCodec = null; String videoResolution = null; - Integer videoBitrate = null; + Integer videoBitrateK = null; String videoFramerate = null; AudioCodec audioCodec = null; Integer audioSampleRate = null; - Integer audioBitrate = null; + Integer audioBitrateK = null; int audioChannels = 2; /* * Example output: - Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'ExampleVideo.mp4': - Metadata: - major_brand : mp42 - minor_version : 0 - compatible_brands: isommp42 - creation_time : 2018-01-02 00:09:32 - com.android.version: 7.1.2 - Duration: 00:02:22.86, start: 0.000000, bitrate: 4569 kb/s - Stream #0:0(eng): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(tv, bt709), 1080x1920, 4499 kb/s, SAR 1:1 DAR 9:16, 19.01 fps, 90k tbr, 90k tbn, 180k tbc (default) - Metadata: - creation_time : 2018-01-02 00:09:32 - handler_name : VideoHandle - Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 22050 Hz, mono, fltp, 63 kb/s (default) - Metadata: - creation_time : 2018-01-02 00:09:32 - handler_name : SoundHandle + { + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "Main", + "codec_type": "video", + "codec_time_base": "1/50", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 640, + "height": 360, + "coded_width": 640, + "coded_height": 360, + "has_b_frames": 0, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "16:9", + "pix_fmt": "yuv420p", + "level": 30, + "chroma_location": "left", + "refs": 1, + "is_avc": "true", + "nal_length_size": "4", + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "bits_per_raw_sample": "8", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "profile": "LC", + "codec_type": "audio", + "codec_time_base": "1/44100", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 6, + "channel_layout": "5.1", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 3, + "start_time": "0.003000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + } + } + ], + "format": { + "filename": "/Users/brarcher/Downloads/big_buck_bunny_360p_1mb.flv", + "nb_streams": 2, + "nb_programs": 0, + "format_name": "flv", + "format_long_name": "FLV (Flash Video)", + "start_time": "0.000000", + "duration": "6.893000", + "size": "1048720", + "bit_rate": "1217142", + "probe_score": 100, + "tags": { + "encoder": "Lavf53.24.2" + } + } + } */ - for(String line : string.split("\n")) + ObjectMapper mapper = new ObjectMapper(); + try { - line = line.trim(); - String [] split; + JsonNode root = mapper.readTree(mediaDetailsJsonStr); - if(line.startsWith("Duration:")) + JsonNode format = root.get("format"); + if(format != null) { - // Duration: 00:02:22.86, start: 0.000000, bitrate: 4569 kb/s - - split = line.split(" "); - if(split.length <= 1) + JsonNode duration = format.get("duration"); + if(duration != null) { - continue; + durationMs = (int)(duration.asDouble() * 1000); } - String valueStr = split[1]; - valueStr = valueStr.replace(",", ""); - - Long time = timestampToMs(valueStr); - if(time == null) + JsonNode bitrate = format.get("bit_rate"); + if(bitrate != null) { - continue; + totalBitrateK = bitrate.asInt()/1000; } - durationMs = time; - - split = line.split(","); - for(String item : split) + JsonNode formatName = format.get("format_name"); + if(formatName != null) { - if(item.contains("bitrate:")) + String formatNameStr = formatName.asText(); + + for(MediaContainer item : MediaContainer.values()) { - item = item.replace("bitrate:", "").replace("kb/s", "").trim(); - try + if(formatNameStr.contains(item.ffmpegName)) { - // This may be used later if the video bitrate cannot be determined. - totalBitrate = Integer.parseInt(item); - } - catch(NumberFormatException e) - { - continue; + container = item; + break; } } } } - if(line.startsWith("Input")) + JsonNode streams = root.get("streams"); + if(streams != null) { - for(MediaContainer item : MediaContainer.values()) + for(int index = 0; index < streams.size(); index++) { - if(line.contains(item.ffmpegName)) + JsonNode stream = streams.get(index); + JsonNode codecType = stream.get("codec_type"); + if(codecType == null) { - container = item; - break; + continue; } - } - } - - if(line.startsWith("Stream") && line.contains("Video:")) - { - // Stream #0:0: Video: h264 (Main), yuv420p, 640x360 [SAR 1:1 DAR 16:9], 25 fps, 25 tbr, 1k tbn, 50 tbc - // Stream #0:0(eng): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(tv, bt709), 1080x1920, 4499 kb/s, SAR 1:1 DAR 9:16, 19.01 fps, 90k tbr, 90k tbn, 180k tbc (default) - - split = line.split(" "); - if(split.length <= 4) - { - continue; - } - String videoCodecName = split[3]; - videoCodec = VideoCodec.fromName(videoCodecName); - - // Looking for resolution. There are sometimes items such as: - // (mp4a / 0x6134706D) - // that have numbers and an 'x', that need to be avoided. - Pattern p = Pattern.compile("[0-9]+x[0-9]+[ ,]{1}"); - Matcher m = p.matcher(line); - - if(m.find()) - { - videoResolution = m.group(0); - // There will be an extra space or , at the end; strip it - videoResolution = videoResolution.trim().replace(",",""); - } - - split = line.split(","); - for(String piece : split) - { - piece = piece.trim(); - - if(piece.contains("kb/s")) + if(codecType.asText().equals("video")) { - try + JsonNode codecName = stream.get("codec_name"); + if(codecName == null) { - String videoBitrateStr = piece.replace("kb/s", "").trim(); - videoBitrate = Integer.parseInt(videoBitrateStr); + continue; } - catch(NumberFormatException e) + videoCodec = VideoCodec.fromName(codecName.asText()); + + JsonNode width = stream.get("width"); + JsonNode height = stream.get("height"); + if(width == null || height == null) { - // Nothing to do + continue; } - } + videoResolution = width + "x" + height; - if(piece.contains("fps")) - { - videoFramerate = piece.replace("fps", "").trim(); - } - } - } - - if(line.startsWith("Stream") && line.contains("Audio:")) - { - // Stream #0:1: Audio: aac (LC), 48000 Hz, 5.1, fltp - // Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 22050 Hz, mono, fltp, 63 kb/s (default) - - split = line.split(" "); - if(split.length <= 4) - { - continue; - } - - String audioCodecName = split[3]; - audioCodec = AudioCodec.fromName(audioCodecName); + // bit_rate may not always be available, so do not require it. + JsonNode bitrate = stream.get("bit_rate"); + if(bitrate != null) + { + videoBitrateK = bitrate.asInt()/1000; + } - split = line.split(","); - for(String piece : split) - { - piece = piece.trim(); + JsonNode frameRate = stream.get("avg_frame_rate"); + if(frameRate == null) + { + continue; + } - if(piece.contains("Hz")) - { try { - String audioSampeRateStr = piece.replace("Hz", "").trim(); - audioSampleRate = Integer.parseInt(audioSampeRateStr); + String frameRateStr = frameRate.asText(); + String [] frameRateSplit = frameRateStr.split("/"); + int frameRateNum = Integer.parseInt(frameRateSplit[0]); + int frameRateDem = 1; + if(frameRateSplit.length > 1) + { + frameRateDem = Integer.parseInt(frameRateSplit[1]); + } + + double frameRateValue = frameRateNum/(double)frameRateDem; + + videoFramerate = String.format(Locale.US,"%.2f", frameRateValue); + if(videoFramerate.contains(".00")) + { + videoFramerate = videoFramerate.replace(".00", ""); + } } catch(NumberFormatException e) { - // Nothing to do + continue; } } - if(piece.contains("kb/s")) + if(codecType.asText().equals("audio")) { - String audioBitrateStr = piece.replace("kb/s", "").trim(); - - if(audioBitrateStr.contains("(default)")) + JsonNode codecName = stream.get("codec_name"); + if(codecName == null) { - audioBitrateStr = audioBitrateStr.replace("(default)", "").trim(); + continue; } + audioCodec = AudioCodec.fromName(codecName.asText()); - try + JsonNode sampleRate = stream.get("sample_rate"); + if(sampleRate == null) { - audioBitrate = Integer.parseInt(audioBitrateStr); + continue; } - catch(NumberFormatException e) + audioSampleRate = sampleRate.asInt(); + + // bit_rate may not always be available, so do not require it. + JsonNode bitrate = stream.get("bit_rate"); + if(bitrate != null) { - // Nothing to do + audioBitrateK = bitrate.asInt()/1000; } - } - if(piece.contains("mono")) - { - audioChannels = 1; + JsonNode channelLaoyout = stream.get("channel_layout"); + if(channelLaoyout == null) + { + continue; + } + audioChannels = channelLaoyout.asText().equals("mono") ? 1 : 2; } } } } + catch (IOException e) + { + Log.w(TAG, "Failed to read media details for file : " + mediaFile.getAbsolutePath() + "\n" + mediaDetailsJsonStr); + } - if(totalBitrate != null) + if(totalBitrateK != null) { - if(videoBitrate == null) + if(videoBitrateK == null) { - if(audioBitrate != null) + if(audioBitrateK != null) { // We know the audio bitrate, we can calculate the video bitrate - videoBitrate = totalBitrate - audioBitrate; + videoBitrateK = totalBitrateK - audioBitrateK; } - if(videoBitrate == null) + if(videoBitrateK == null) { // We do not know any of the separate bitrates. Lets guess 100 kb/s for the audio, // and subtract that from the total to guess the video bitrate. @@ -417,13 +518,13 @@ static MediaInfo parseMediaInfo(File mediaFile, String string) // As a guess, subtract 100 kb/s from the bitrate for audio, and // assume that the video is the rest. This should be a decent-ish // estimate if the video bitrate cannot be found later. - videoBitrate = totalBitrate - 100; + videoBitrateK = totalBitrateK - 100; } } } MediaInfo info = new MediaInfo(mediaFile, durationMs, container, videoCodec, videoResolution, - videoBitrate, videoFramerate, audioCodec, audioSampleRate, audioBitrate, audioChannels); + videoBitrateK, videoFramerate, audioCodec, audioSampleRate, audioBitrateK, audioChannels); return info; } diff --git a/app/src/main/java/protect/videotranscoder/activity/MainActivity.java b/app/src/main/java/protect/videotranscoder/activity/MainActivity.java index 87f40f69..66cd8912 100644 --- a/app/src/main/java/protect/videotranscoder/activity/MainActivity.java +++ b/app/src/main/java/protect/videotranscoder/activity/MainActivity.java @@ -42,6 +42,7 @@ import com.google.common.collect.ImmutableMap; import com.crystal.crystalrangeseekbar.widgets.CrystalRangeSeekbar; +import org.javatuples.Triplet; import java.io.File; import java.lang.ref.WeakReference; @@ -236,6 +237,16 @@ public void onServiceConnected(ComponentName className, IBinder service) { selectVideoButton.setEnabled(true); } + + Intent intent = getIntent(); + if(intent != null) + { + String action = intent.getAction(); + if(action != null && action.equals("protect.videotranscoder.ENCODE")) + { + handleEncodeIntent(intent); + } + } } catch (RemoteException e) { @@ -254,6 +265,130 @@ public void onServiceDisconnected(ComponentName className) } }; + @Override + public void onNewIntent(Intent intent) + { + setIntent(intent); + String action = intent.getAction(); + if(action != null && action.contains("ENCODE")) + { + handleEncodeIntent(intent); + } + } + + private void handleEncodeIntent(Intent intent) + { + final String inputFilePath = intent.getStringExtra("inputVideoFilePath"); + final String destinationFilePath = intent.getStringExtra("outputFilePath"); + + String mediaContainerStr = intent.getStringExtra("mediaContainer"); + final MediaContainer container = MediaContainer.fromName(mediaContainerStr); + + String videoCodecStr = intent.getStringExtra("videoCodec"); + final VideoCodec videoCodec = VideoCodec.fromName(videoCodecStr); + + int tmpCideoBitrateK = intent.getIntExtra("videoBitrateK", -1); + final Integer videoBitrateK = tmpCideoBitrateK != -1 ? tmpCideoBitrateK : null; + + final String resolution = intent.getStringExtra("resolution"); + final String fps = intent.getStringExtra("fps"); + + String audioCodecStr = intent.getStringExtra("audioCodec"); + final AudioCodec audioCodec = AudioCodec.fromName(audioCodecStr); + + int tmpAudioSampleRate = intent.getIntExtra("audioSampleRate", -1); + final Integer audioSampleRate = tmpAudioSampleRate != -1 ? tmpAudioSampleRate : null; + + final String audioChannel = intent.getStringExtra("audioChannel"); + + int tmpAudioBitrateK = intent.getIntExtra("audioBitrateK", -1); + final Integer audioBitrateK = tmpAudioBitrateK != -1 ? tmpAudioBitrateK : null; + + boolean skipDialog = intent.getBooleanExtra("skipDialog", false); + + List> nullChecks = new LinkedList<>(); + nullChecks.add(new Triplet<>((Object)inputFilePath, R.string.fieldMissingError, "inputFilePath")); + nullChecks.add(new Triplet<>((Object)destinationFilePath, R.string.fieldMissingError, "outputFilePath")); + nullChecks.add(new Triplet<>((Object)container, R.string.fieldMissingOrInvalidError, "mediaContainer")); + if(container != null && container.supportedVideoCodecs.size() > 0) + { + nullChecks.add(new Triplet<>((Object)videoCodec, R.string.fieldMissingOrInvalidError, "videoCodec")); + nullChecks.add(new Triplet<>((Object)videoBitrateK, R.string.fieldMissingError, "videoBitrateK missing")); + nullChecks.add(new Triplet<>((Object)resolution, R.string.fieldMissingError, "resolution")); + nullChecks.add(new Triplet<>((Object)fps, R.string.fieldMissingError, "fps")); + } + if(container != null && container.supportedAudioCodecs.size() > 0) + { + nullChecks.add(new Triplet<>((Object)audioCodec, R.string.fieldMissingOrInvalidError, "audioCodec")); + nullChecks.add(new Triplet<>((Object)audioSampleRate, R.string.fieldMissingError, "audioSampleRate")); + nullChecks.add(new Triplet<>((Object)audioChannel, R.string.fieldMissingError, "audioChannel")); + nullChecks.add(new Triplet<>((Object)audioBitrateK, R.string.fieldMissingError, "audioBitrateK")); + } + + for(Triplet check : nullChecks) + { + if(check.getValue0() == null) + { + String submsg = String.format(getString(check.getValue1()), check.getValue2()); + String message = String.format(getString(R.string.cannotEncodeFile), submsg); + Log.i(TAG, message); + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + finish(); + return; + } + } + + String message = String.format(getString(R.string.encodeStartConfirmation), inputFilePath, destinationFilePath); + + DialogInterface.OnClickListener positiveButtonListener = new DialogInterface.OnClickListener() + { + public void onClick(final DialogInterface dialog, int which) + { + FFmpegUtil.getMediaDetails(new File(inputFilePath), new ResultCallbackHandler() + { + @Override + public void onResult(MediaInfo result) + { + if(result != null) + { + startEncode(inputFilePath, 0, (int)(result.durationMs/1000), container, videoCodec, videoBitrateK, + resolution, fps, audioCodec, audioSampleRate, audioChannel, audioBitrateK, destinationFilePath); + } + else + { + String message = String.format(getString(R.string.transcodeFailed), getString(R.string.couldNotFindFileSubmsg)); + Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show(); + finish(); + dialog.dismiss(); + } + } + }); + } + }; + + AlertDialog dialog = new AlertDialog.Builder(this) + .setMessage(message) + .setCancelable(false) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + finish(); + dialog.dismiss(); + } + }) + .setPositiveButton(R.string.encode, positiveButtonListener).create(); + + dialog.show(); + + if(skipDialog) + { + positiveButtonListener.onClick(dialog, 0); + dialog.dismiss(); + } + } + private void showUnsupportedExceptionDialog() { new AlertDialog.Builder(this) @@ -374,73 +509,11 @@ public void onSelect(String filePath) fileChooser.show(); } - private void startEncode() + private List getFfmpegEncodingArgs(String inputFilePath, Integer startTimeSec, Integer endTimeSec, + MediaContainer container, VideoCodec videoCodec, Integer videoBitrateK, + String resolution, String fps, AudioCodec audioCodec, Integer audioSampleRate, + String audioChannel, Integer audioBitrateK, String destinationFilePath) { - MediaContainer container = (MediaContainer)containerSpinner.getSelectedItem(); - VideoCodecWrapper videoCodecWrapper = (VideoCodecWrapper)videoCodecSpinner.getSelectedItem(); - VideoCodec videoCodec = videoCodecWrapper != null ? videoCodecWrapper.codec : null; - String fps = (String)fpsSpinner.getSelectedItem(); - String resolution = (String)resolutionSpinner.getSelectedItem(); - AudioCodec audioCodec = (AudioCodec) audioCodecSpinner.getSelectedItem(); - Integer audioBitrate = (Integer) audioBitrateSpinner.getSelectedItem(); - Integer audioSampleRate = (Integer) audioSampleRateSpinner.getSelectedItem(); - String audioChannel = (String) audioChannelSpinner.getSelectedItem(); - int videoBitrate; - - if(videoInfo == null) - { - Toast.makeText(this, R.string.selectFileFirst, Toast.LENGTH_LONG).show(); - return; - } - - try - { - String videoBitrateStr = videoBitrateValue.getText().toString(); - videoBitrate = Integer.parseInt(videoBitrateStr); - } - catch(NumberFormatException e) - { - Toast.makeText(this, R.string.videoBitrateValueInvalid, Toast.LENGTH_LONG).show(); - return; - } - - File outputDir; - - if(container.supportedVideoCodecs.size() > 0) - { - outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); - } - else - { - outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); - } - - if(outputDir.exists() == false) - { - boolean result = outputDir.mkdirs(); - if(result == false) - { - Log.w(TAG, "Unable to create destination dir: " + outputDir.getAbsolutePath()); - } - } - - String filePrefix = videoInfo.file.getName(); - if(filePrefix.contains(".")) - { - filePrefix = filePrefix.substring(0, filePrefix.lastIndexOf(".")); - } - - String extension = "." + container.extension; - String inputFilePath = videoInfo.file.getAbsolutePath(); - - File destination = new File(outputDir, filePrefix + extension); - int fileNo = 0; - while (destination.exists()) - { - fileNo++; - destination = new File(outputDir, filePrefix + "_" + fileNo + extension); - } - List command = new LinkedList<>(); // If the output exists, overwrite it @@ -450,27 +523,29 @@ private void startEncode() command.add("-i"); command.add(inputFilePath); - int startTimeSec = rangeSeekBar.getSelectedMinValue().intValue(); - if(startTimeSec != 0) + if (startTimeSec != null && startTimeSec != 0) { // Start time offset command.add("-ss"); command.add(Integer.toString(startTimeSec)); } - int endTimeSec = rangeSeekBar.getSelectedMaxValue().intValue(); - int durationSec = endTimeSec - startTimeSec; - if( (videoInfo.durationMs)/1000 != endTimeSec) + if(startTimeSec != null && endTimeSec != null) { - // Duration of media file - command.add("-t"); - command.add(Integer.toString(durationSec)); + int durationSec = endTimeSec - startTimeSec; + + if (durationSec != endTimeSec) + { + // Duration of media file + command.add("-t"); + command.add(Integer.toString(durationSec)); + } } - if(container.supportedVideoCodecs.size() > 0) + if (container.supportedVideoCodecs.size() > 0) { // These options only apply when not using GIF - if(videoCodec != VideoCodec.GIF) + if (videoCodec != VideoCodec.GIF) { // Video codec command.add("-vcodec"); @@ -478,7 +553,7 @@ private void startEncode() // Video bitrate command.add("-b:v"); - command.add(videoBitrate + "k"); + command.add(videoBitrateK + "k"); } // Frame size @@ -488,20 +563,19 @@ private void startEncode() // Frame rate command.add("-r"); command.add(fps); - } - else + } else { // No video command.add("-vn"); } - if(container.supportedAudioCodecs.size() > 0 && audioCodec != AudioCodec.NONE) + if (container.supportedAudioCodecs.size() > 0 && audioCodec != AudioCodec.NONE) { // Audio codec command.add("-acodec"); command.add(audioCodec.ffmpegName); - if(audioCodec == AudioCodec.VORBIS) + if (audioCodec == AudioCodec.VORBIS) { // The vorbis encode is experimental, and needs other // flags to enable @@ -519,7 +593,7 @@ private void startEncode() // Audio bitrate command.add("-b:a"); - command.add(audioBitrate + "k"); + command.add(audioBitrateK + "k"); } else { @@ -527,14 +601,26 @@ private void startEncode() command.add("-an"); } - if(container == MediaContainer.GIF) + if (container == MediaContainer.GIF) { command.add("-filter_complex"); command.add("fps=" + fps + ",split [o1] [o2];[o1] palettegen [p]; [o2] fifo [o3];[o3] [p] paletteuse"); } // Output file - command.add(destination.getAbsolutePath()); + command.add(destinationFilePath); + + return command; + } + + private void startEncode(String inputFilePath, Integer startTimeSec, Integer endTimeSec, + MediaContainer container, VideoCodec videoCodec, Integer videoBitrateK, + String resolution, String fps, AudioCodec audioCodec, Integer audioSampleRate, + String audioChannel, Integer audioBitrateK, String destinationFilePath) + { + List args = getFfmpegEncodingArgs(inputFilePath, startTimeSec, endTimeSec, + container, videoCodec, videoBitrateK, resolution, fps, audioCodec, audioSampleRate, + audioChannel, audioBitrateK, destinationFilePath); updateUiForEncoding(); @@ -542,7 +628,8 @@ private void startEncode() try { Log.d(TAG, "Sending encode request to service"); - success = ffmpegService.startEncode(command, destination.getAbsolutePath(), container.mimetype, durationSec*1000); + int durationSec = endTimeSec - startTimeSec; + success = ffmpegService.startEncode(args, destinationFilePath, container.mimetype, durationSec*1000); } catch (RemoteException e) { @@ -555,6 +642,67 @@ private void startEncode() } } + private void startEncode() + { + MediaContainer container = (MediaContainer)containerSpinner.getSelectedItem(); + VideoCodecWrapper videoCodecWrapper = (VideoCodecWrapper)videoCodecSpinner.getSelectedItem(); + VideoCodec videoCodec = videoCodecWrapper != null ? videoCodecWrapper.codec : null; + String fps = (String)fpsSpinner.getSelectedItem(); + String resolution = (String)resolutionSpinner.getSelectedItem(); + AudioCodec audioCodec = (AudioCodec) audioCodecSpinner.getSelectedItem(); + Integer audioBitrateK = (Integer) audioBitrateSpinner.getSelectedItem(); + Integer audioSampleRate = (Integer) audioSampleRateSpinner.getSelectedItem(); + String audioChannel = (String) audioChannelSpinner.getSelectedItem(); + int videoBitrateK; + + if(videoInfo == null) + { + Toast.makeText(this, R.string.selectFileFirst, Toast.LENGTH_LONG).show(); + return; + } + + try + { + String videoBitrateKStr = videoBitrateValue.getText().toString(); + videoBitrateK = Integer.parseInt(videoBitrateKStr); + } + catch(NumberFormatException e) + { + Toast.makeText(this, R.string.videoBitrateValueInvalid, Toast.LENGTH_LONG).show(); + return; + } + + File outputDir; + + if(container.supportedVideoCodecs.size() > 0) + { + outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); + } + else + { + outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); + } + + String filePrefix = "Video_Transcoder_Output"; + String extension = "." + container.extension; + String inputFilePath = videoInfo.file.getAbsolutePath(); + + File destination = new File(outputDir, filePrefix + extension); + int fileNo = 0; + while (destination.exists()) + { + fileNo++; + destination = new File(outputDir, filePrefix + "_" + fileNo + extension); + } + + int startTimeSec = rangeSeekBar.getSelectedMinValue().intValue(); + int endTimeSec = rangeSeekBar.getSelectedMaxValue().intValue(); + + startEncode(inputFilePath, startTimeSec, endTimeSec, container, videoCodec, + videoBitrateK, resolution, fps, audioCodec, audioSampleRate, audioChannel, + audioBitrateK, destination.getAbsolutePath()); + } + private void updateUiForEncoding() { stopVideoPlayback(); @@ -850,16 +998,16 @@ public void onNothingSelected(AdapterView parentView) } }); - List audioBitrate = new ArrayList<>(Arrays.asList(15, 24, 32, 64, 96, 128, 192, 256, 320, 384, 448, 512)); - if(videoInfo.audioBitrate != null && audioBitrate.contains(videoInfo.audioBitrate) == false) + List audioBitrateK = new ArrayList<>(Arrays.asList(15, 24, 32, 64, 96, 128, 192, 256, 320, 384, 448, 512)); + if(videoInfo.audioBitrateK != null && audioBitrateK.contains(videoInfo.audioBitrateK) == false) { - audioBitrate.add(videoInfo.audioBitrate); - Collections.sort(audioBitrate); + audioBitrateK.add(videoInfo.audioBitrateK); + Collections.sort(audioBitrateK); } - audioBitrateSpinner.setAdapter(new ArrayAdapter<>(this, R.layout.spinner_textview, audioBitrate)); + audioBitrateSpinner.setAdapter(new ArrayAdapter<>(this, R.layout.spinner_textview, audioBitrateK)); List sampleRate = new ArrayList<>(Arrays.asList(8000, 11025, 16000, 22050, 24000, 32000, 44100, 48000)); - if(videoInfo.audioSampleRate != null && audioBitrate.contains(videoInfo.audioSampleRate) == false) + if(videoInfo.audioSampleRate != null && audioBitrateK.contains(videoInfo.audioSampleRate) == false) { sampleRate.add(videoInfo.audioSampleRate); Collections.sort(sampleRate); @@ -889,9 +1037,9 @@ public void onNothingSelected(AdapterView parentView) setSpinnerSelection(resolutionSpinner, videoInfo.videoResolution); } - if(videoInfo.videoBitrate != null) + if(videoInfo.videoBitrateK != null) { - videoBitrateValue.setText(Integer.toString(videoInfo.videoBitrate)); + videoBitrateValue.setText(Integer.toString(videoInfo.videoBitrateK)); } if(videoInfo.audioCodec != null) @@ -899,14 +1047,14 @@ public void onNothingSelected(AdapterView parentView) setSpinnerSelection(audioCodecSpinner, videoInfo.audioCodec.toString()); } - Integer defaultAudioBitrate = videoInfo.audioBitrate; - if(defaultAudioBitrate == null) + Integer defaultAudioBitrateK = videoInfo.audioBitrateK; + if(defaultAudioBitrateK == null) { // Set some default if none is detected - defaultAudioBitrate = 128; + defaultAudioBitrateK = 128; } - setSpinnerSelection(audioBitrateSpinner, defaultAudioBitrate); + setSpinnerSelection(audioBitrateSpinner, defaultAudioBitrateK); if(videoInfo.audioSampleRate != null) { @@ -1140,6 +1288,16 @@ private void showEncodeCompleteDialog(final MainActivity mainActivity, final boo { Log.d(TAG, "Encode result: " + result); + Intent intent = mainActivity.getIntent(); + final String action = intent.getAction(); + boolean skipDialog = intent.getBooleanExtra("skipDialog", false); + + if(skipDialog) + { + mainActivity.finish(); + return; + } + String message; if(result) @@ -1159,7 +1317,16 @@ private void showEncodeCompleteDialog(final MainActivity mainActivity, final boo @Override public void onDismiss(DialogInterface dialog) { - mainActivity.startVideoPlayback(null); + if(action != null && action.contains("ENCODE")) + { + // This activity was launched to encode this file, + // finish it off when complete. + mainActivity.finish(); + } + else + { + mainActivity.startVideoPlayback(null); + } } }) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() @@ -1228,6 +1395,8 @@ private void displayAboutDialog() .put("Guava", "https://github.com/google/guava") .put("Crystal Range Seekbar", "https://github.com/syedowaisali/crystal-range-seekbar") .put("Storage Chooser", "https://github.com/codekidX/storage-chooser") + .put("javatuples", "https://www.javatuples.org/") + .put("jackson-databind", "https://github.com/FasterXML/jackson-databind") .build(); final Map USED_ASSETS = ImmutableMap.of diff --git a/app/src/main/java/protect/videotranscoder/media/AudioCodec.java b/app/src/main/java/protect/videotranscoder/media/AudioCodec.java index d35d122a..189d0578 100644 --- a/app/src/main/java/protect/videotranscoder/media/AudioCodec.java +++ b/app/src/main/java/protect/videotranscoder/media/AudioCodec.java @@ -30,11 +30,14 @@ public enum AudioCodec @Nullable public static AudioCodec fromName(String name) { - for(AudioCodec item : values()) + if(name != null) { - if(item.ffmpegName.equals(name)) + for (AudioCodec item : values()) { - return item; + if (item.ffmpegName.equals(name)) + { + return item; + } } } diff --git a/app/src/main/java/protect/videotranscoder/media/MediaContainer.java b/app/src/main/java/protect/videotranscoder/media/MediaContainer.java index 30d486ff..7f8e7b7d 100644 --- a/app/src/main/java/protect/videotranscoder/media/MediaContainer.java +++ b/app/src/main/java/protect/videotranscoder/media/MediaContainer.java @@ -41,11 +41,14 @@ public enum MediaContainer public static MediaContainer fromName(String ffmpegName) { - for(MediaContainer item : MediaContainer.values()) + if(ffmpegName != null) { - if(item.ffmpegName.endsWith(ffmpegName)) + for (MediaContainer item : MediaContainer.values()) { - return item; + if (item.ffmpegName.endsWith(ffmpegName)) + { + return item; + } } } diff --git a/app/src/main/java/protect/videotranscoder/media/MediaInfo.java b/app/src/main/java/protect/videotranscoder/media/MediaInfo.java index 57643fb6..f24b11ae 100644 --- a/app/src/main/java/protect/videotranscoder/media/MediaInfo.java +++ b/app/src/main/java/protect/videotranscoder/media/MediaInfo.java @@ -12,17 +12,17 @@ public class MediaInfo public final MediaContainer container; public final VideoCodec videoCodec; public final String videoResolution; - public final Integer videoBitrate; + public final Integer videoBitrateK; public final String videoFramerate; public final AudioCodec audioCodec; public final Integer audioSampleRate; - public final Integer audioBitrate; + public final Integer audioBitrateK; public final Integer audioChannels; public MediaInfo(File file, long durationMs, MediaContainer container, VideoCodec videoCodec, - String videoResolution, Integer videoBitrate, String videoFramerate, - AudioCodec audioCodec, Integer audioSampleRate, Integer audioBitrate, + String videoResolution, Integer videoBitrateK, String videoFramerate, + AudioCodec audioCodec, Integer audioSampleRate, Integer audioBitrateK, Integer audioChannels) { this.file = file; @@ -30,11 +30,11 @@ public MediaInfo(File file, long durationMs, MediaContainer container, VideoCode this.container = container; this.videoCodec = videoCodec; this.videoResolution = videoResolution; - this.videoBitrate = videoBitrate; + this.videoBitrateK = videoBitrateK; this.videoFramerate = videoFramerate; this.audioCodec = audioCodec; this.audioSampleRate = audioSampleRate; - this.audioBitrate = audioBitrate; + this.audioBitrateK = audioBitrateK; this.audioChannels = audioChannels; } } diff --git a/app/src/main/java/protect/videotranscoder/media/VideoCodec.java b/app/src/main/java/protect/videotranscoder/media/VideoCodec.java index e3cad335..d01846af 100644 --- a/app/src/main/java/protect/videotranscoder/media/VideoCodec.java +++ b/app/src/main/java/protect/videotranscoder/media/VideoCodec.java @@ -30,11 +30,14 @@ public enum VideoCodec @Nullable public static VideoCodec fromName(String name) { - for(VideoCodec item : values()) + if(name != null) { - if(item.ffmpegName.equals(name)) + for (VideoCodec item : values()) { - return item; + if (item.ffmpegName.equals(name)) + { + return item; + } } } diff --git a/app/src/main/java/protect/videotranscoder/service/FFmpegProcessService.java b/app/src/main/java/protect/videotranscoder/service/FFmpegProcessService.java index 5f18cdf5..b5c95230 100644 --- a/app/src/main/java/protect/videotranscoder/service/FFmpegProcessService.java +++ b/app/src/main/java/protect/videotranscoder/service/FFmpegProcessService.java @@ -16,6 +16,7 @@ import java.util.List; import nl.bravobit.ffmpeg.ExecuteBinaryResponseHandler; +import nl.bravobit.ffmpeg.FFprobe; import protect.videotranscoder.FFmpegUtil; import protect.videotranscoder.R; import protect.videotranscoder.activity.MainActivity; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b81419cc..197672fc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,8 +27,13 @@ You must select a file to encode first. Video bitrate value is invalid, could not encode. Encoding %s + Do you want to encode %s and output to %s? Encoding completed successfully. The file was written to:\n%s Encoding failed: %s + Cannot encode file: %s + could not find input file + %s missing + %s missing or invalid This application needs access to storage in order to read and write media files. Without this access, the application cannot transcode. Request again diff --git a/app/src/test/java/protect/videotranscoder/FFmpegUtilTest.java b/app/src/test/java/protect/videotranscoder/FFmpegUtilTest.java index 6103bb2d..4eae0242 100644 --- a/app/src/test/java/protect/videotranscoder/FFmpegUtilTest.java +++ b/app/src/test/java/protect/videotranscoder/FFmpegUtilTest.java @@ -24,102 +24,424 @@ public void parseMediaInfo() throws Exception { File file = new File("/dev/null"); - String string = "Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'ExampleVideo.mp4'\n" + - "Metadata:\n" + - "major_brand : mp42\n" + - "minor_version : 0\n" + - "compatible_brands: isommp42\n" + - "creation_time : 2018-01-02 00:09:32\n" + - "com.android.version: 7.1.2\n" + - "Duration: 00:02:22.86, start: 0.000000, bitrate: 4569 kb/s\n" + - "Stream #0:0(eng): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(tv, bt709), 1080x1920, 4499 kb/s, SAR 1:1 DAR 9:16, 19.01 fps, 90k tbr, 90k tbn, 180k tbc (default)\n" + - "Metadata:\n" + - "creation_time : 2018-01-02 00:09:32\n" + - "handler_name : VideoHandle\n" + - "Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 22050 Hz, mono, fltp, 63 kb/s (default)\n" + - "Metadata:\n" + - "creation_time : 2018-01-02 00:09:32\n" + - "handler_name : SoundHandle\n"; - + String string = "{\n" + + " \"streams\": [\n" + + " {\n" + + " \"index\": 0,\n" + + " \"codec_name\": \"mp3\",\n" + + " \"codec_long_name\": \"MP3 (MPEG audio layer 3)\",\n" + + " \"codec_type\": \"audio\",\n" + + " \"codec_time_base\": \"1/44100\",\n" + + " \"codec_tag_string\": \"[0][0][0][0]\",\n" + + " \"codec_tag\": \"0x0000\",\n" + + " \"sample_fmt\": \"s16p\",\n" + + " \"sample_rate\": \"44100\",\n" + + " \"channels\": 2,\n" + + " \"channel_layout\": \"stereo\",\n" + + " \"bits_per_sample\": 0,\n" + + " \"r_frame_rate\": \"0/0\",\n" + + " \"avg_frame_rate\": \"0/0\",\n" + + " \"time_base\": \"1/14112000\",\n" + + " \"start_pts\": 353600,\n" + + " \"start_time\": \"0.025057\",\n" + + " \"duration_ts\": 63109324800,\n" + + " \"duration\": \"4472.032653\",\n" + + " \"bit_rate\": \"128000\",\n" + + " \"disposition\": {\n" + + " \"default\": 0,\n" + + " \"dub\": 0,\n" + + " \"original\": 0,\n" + + " \"comment\": 0,\n" + + " \"lyrics\": 0,\n" + + " \"karaoke\": 0,\n" + + " \"forced\": 0,\n" + + " \"hearing_impaired\": 0,\n" + + " \"visual_impaired\": 0,\n" + + " \"clean_effects\": 0,\n" + + " \"attached_pic\": 0\n" + + " },\n" + + " \"tags\": {\n" + + " \"encoder\": \"Lavc57.48\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"format\": {\n" + + " \"filename\": \"/Users/test/Downloads/recording.mp3\",\n" + + " \"nb_streams\": 1,\n" + + " \"nb_programs\": 0,\n" + + " \"format_name\": \"mp3\",\n" + + " \"format_long_name\": \"MP2/3 (MPEG audio layer 2/3)\",\n" + + " \"start_time\": \"0.025057\",\n" + + " \"duration\": \"4472.032653\",\n" + + " \"size\": \"71553077\",\n" + + " \"bit_rate\": \"128000\",\n" + + " \"probe_score\": 51,\n" + + " \"tags\": {\n" + + " \"major_brand\": \"dash\",\n" + + " \"minor_version\": \"0\",\n" + + " \"compatible_brands\": \"iso6mp41\",\n" + + " \"encoder\": \"Lavf57.41.100\"\n" + + " }\n" + + " }\n" + + "}"; MediaInfo info = FFmpegUtil.parseMediaInfo(file, string); assertEquals(file, info.file); - assertEquals( (2*60+22)*1000 + 86, info.durationMs); - assertEquals(MediaContainer.MP4, info.container); - assertEquals(VideoCodec.H264, info.videoCodec); - assertEquals("1080x1920", info.videoResolution); - assertEquals(Integer.valueOf(4499), info.videoBitrate); - assertEquals("19.01", info.videoFramerate); - assertEquals(AudioCodec.AAC, info.audioCodec); - assertEquals(Integer.valueOf(22050), info.audioSampleRate); - assertEquals(Integer.valueOf(63), info.audioBitrate); - assertEquals(Integer.valueOf(1), info.audioChannels); + assertEquals( 4472032, info.durationMs); + assertEquals(MediaContainer.MP3, info.container); + assertEquals(null, info.videoCodec); + assertEquals(null, info.videoResolution); + assertEquals(Integer.valueOf(0), info.videoBitrateK); + assertEquals(null, info.videoFramerate); + assertEquals(AudioCodec.MP3, info.audioCodec); + assertEquals(Integer.valueOf(44100), info.audioSampleRate); + assertEquals(Integer.valueOf(128), info.audioBitrateK); + assertEquals(Integer.valueOf(2), info.audioChannels); + - string = "libavutil 55. 17.103 / 55. 17.103\n" + - "libavcodec 57. 24.102 / 57. 24.102\n" + - "libavformat 57. 25.100 / 57. 25.100\n" + - "libavdevice 57. 0.101 / 57. 0.101\n" + - "libavfilter 6. 31.100 / 6. 31.100\n" + - "libswscale 4. 0.100 / 4. 0.100\n" + - "libswresample 2. 0.101 / 2. 0.101\n" + - "libpostproc 54. 0.100 / 54. 0.100\n" + - "Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/storage/self/primary/Movies/cut_video.mp4':\n" + - "Metadata:\n" + - " major_brand : isom\n" + - " minor_version : 512\n" + - " compatible_brands: isomiso2mp41\n" + - " encoder : Lavf57.25.100\n" + - "Duration: 00:00:10.05, start: 0.035420, bitrate: 754 kb/s\n" + - " Stream #0:0(und): Video: mpeg4 (Simple Profile) (mp4v / 0x7634706D), yuv420p, 320x240 [SAR 1:1 DAR 4:3], 705 kb/s, 25 fps, 25 tbr, 12800 tbn, 25 tbc (default)\n" + - " Metadata:\n" + - " handler_name : VideoHandler\n" + - " Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 22050 Hz, stereo, fltp, 47 kb/s (default)\n" + - " Metadata:\n" + - " handler_name : SoundHandler"; + string = "{\n" + + " \"streams\": [\n" + + " {\n" + + " \"index\": 0,\n" + + " \"codec_name\": \"h264\",\n" + + " \"codec_long_name\": \"H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10\",\n" + + " \"profile\": \"Constrained Baseline\",\n" + + " \"codec_type\": \"video\",\n" + + " \"codec_time_base\": \"12864731/489240000\",\n" + + " \"codec_tag_string\": \"avc1\",\n" + + " \"codec_tag\": \"0x31637661\",\n" + + " \"width\": 1080,\n" + + " \"height\": 1920,\n" + + " \"coded_width\": 1080,\n" + + " \"coded_height\": 1920,\n" + + " \"has_b_frames\": 0,\n" + + " \"sample_aspect_ratio\": \"1:1\",\n" + + " \"display_aspect_ratio\": \"9:16\",\n" + + " \"pix_fmt\": \"yuv420p\",\n" + + " \"level\": 40,\n" + + " \"color_range\": \"tv\",\n" + + " \"color_space\": \"bt709\",\n" + + " \"color_transfer\": \"bt709\",\n" + + " \"color_primaries\": \"bt709\",\n" + + " \"chroma_location\": \"left\",\n" + + " \"refs\": 1,\n" + + " \"is_avc\": \"true\",\n" + + " \"nal_length_size\": \"4\",\n" + + " \"r_frame_rate\": \"180000/2\",\n" + + " \"avg_frame_rate\": \"244620000/12864731\",\n" + + " \"time_base\": \"1/90000\",\n" + + " \"start_pts\": 0,\n" + + " \"start_time\": \"0.000000\",\n" + + " \"duration_ts\": 12864731,\n" + + " \"duration\": \"142.941456\",\n" + + " \"bit_rate\": \"4499166\",\n" + + " \"bits_per_raw_sample\": \"8\",\n" + + " \"nb_frames\": \"2718\",\n" + + " \"disposition\": {\n" + + " \"default\": 1,\n" + + " \"dub\": 0,\n" + + " \"original\": 0,\n" + + " \"comment\": 0,\n" + + " \"lyrics\": 0,\n" + + " \"karaoke\": 0,\n" + + " \"forced\": 0,\n" + + " \"hearing_impaired\": 0,\n" + + " \"visual_impaired\": 0,\n" + + " \"clean_effects\": 0,\n" + + " \"attached_pic\": 0\n" + + " },\n" + + " \"tags\": {\n" + + " \"creation_time\": \"2018-01-02 00:09:32\",\n" + + " \"language\": \"eng\",\n" + + " \"handler_name\": \"VideoHandle\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"index\": 1,\n" + + " \"codec_name\": \"aac\",\n" + + " \"codec_long_name\": \"AAC (Advanced Audio Coding)\",\n" + + " \"profile\": \"LC\",\n" + + " \"codec_type\": \"audio\",\n" + + " \"codec_time_base\": \"1/22050\",\n" + + " \"codec_tag_string\": \"mp4a\",\n" + + " \"codec_tag\": \"0x6134706d\",\n" + + " \"sample_fmt\": \"fltp\",\n" + + " \"sample_rate\": \"22050\",\n" + + " \"channels\": 1,\n" + + " \"channel_layout\": \"mono\",\n" + + " \"bits_per_sample\": 0,\n" + + " \"r_frame_rate\": \"0/0\",\n" + + " \"avg_frame_rate\": \"0/0\",\n" + + " \"time_base\": \"1/44100\",\n" + + " \"start_pts\": 0,\n" + + " \"start_time\": \"0.000000\",\n" + + " \"duration_ts\": 6286221,\n" + + " \"duration\": \"142.544694\",\n" + + " \"bit_rate\": \"63860\",\n" + + " \"nb_frames\": \"3064\",\n" + + " \"disposition\": {\n" + + " \"default\": 1,\n" + + " \"dub\": 0,\n" + + " \"original\": 0,\n" + + " \"comment\": 0,\n" + + " \"lyrics\": 0,\n" + + " \"karaoke\": 0,\n" + + " \"forced\": 0,\n" + + " \"hearing_impaired\": 0,\n" + + " \"visual_impaired\": 0,\n" + + " \"clean_effects\": 0,\n" + + " \"attached_pic\": 0\n" + + " },\n" + + " \"tags\": {\n" + + " \"creation_time\": \"2018-01-02 00:09:32\",\n" + + " \"language\": \"eng\",\n" + + " \"handler_name\": \"SoundHandle\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"format\": {\n" + + " \"filename\": \"/Users/test/Downloads/ScreenRecord-2018-01-01-19-07-10.mp4\",\n" + + " \"nb_streams\": 2,\n" + + " \"nb_programs\": 0,\n" + + " \"format_name\": \"mov,mp4,m4a,3gp,3g2,mj2\",\n" + + " \"format_long_name\": \"QuickTime / MOV\",\n" + + " \"start_time\": \"0.000000\",\n" + + " \"duration\": \"142.862000\",\n" + + " \"size\": \"81603639\",\n" + + " \"bit_rate\": \"4569648\",\n" + + " \"probe_score\": 100,\n" + + " \"tags\": {\n" + + " \"major_brand\": \"mp42\",\n" + + " \"minor_version\": \"0\",\n" + + " \"compatible_brands\": \"isommp42\",\n" + + " \"creation_time\": \"2018-01-02 00:09:32\",\n" + + " \"com.android.version\": \"7.1.2\"\n" + + " }\n" + + " }\n" + + "}\n"; info = FFmpegUtil.parseMediaInfo(file, string); assertEquals(file, info.file); - assertEquals( (10)*1000 + 5, info.durationMs); + assertEquals( 142862, info.durationMs); assertEquals(MediaContainer.MP4, info.container); - assertEquals(VideoCodec.MPEG4, info.videoCodec); - assertEquals("320x240", info.videoResolution); - assertEquals(Integer.valueOf(705), info.videoBitrate); - assertEquals("25", info.videoFramerate); + assertEquals(VideoCodec.H264, info.videoCodec); + assertEquals("1080x1920", info.videoResolution); + assertEquals(Integer.valueOf(4499), info.videoBitrateK); + assertEquals("19.01", info.videoFramerate); assertEquals(AudioCodec.AAC, info.audioCodec); assertEquals(Integer.valueOf(22050), info.audioSampleRate); - assertEquals(Integer.valueOf(47), info.audioBitrate); - assertEquals(Integer.valueOf(2), info.audioChannels); + assertEquals(Integer.valueOf(63), info.audioBitrateK); + assertEquals(Integer.valueOf(1), info.audioChannels); + + string = "{\n" + + " \"streams\": [\n" + + " {\n" + + " \"index\": 0,\n" + + " \"codec_name\": \"h264\",\n" + + " \"codec_long_name\": \"H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10\",\n" + + " \"profile\": \"Main\",\n" + + " \"codec_type\": \"video\",\n" + + " \"codec_time_base\": \"1/50\",\n" + + " \"codec_tag_string\": \"[0][0][0][0]\",\n" + + " \"codec_tag\": \"0x0000\",\n" + + " \"width\": 640,\n" + + " \"height\": 360,\n" + + " \"coded_width\": 640,\n" + + " \"coded_height\": 360,\n" + + " \"has_b_frames\": 0,\n" + + " \"sample_aspect_ratio\": \"1:1\",\n" + + " \"display_aspect_ratio\": \"16:9\",\n" + + " \"pix_fmt\": \"yuv420p\",\n" + + " \"level\": 30,\n" + + " \"chroma_location\": \"left\",\n" + + " \"refs\": 1,\n" + + " \"is_avc\": \"true\",\n" + + " \"nal_length_size\": \"4\",\n" + + " \"r_frame_rate\": \"25/1\",\n" + + " \"avg_frame_rate\": \"25/1\",\n" + + " \"time_base\": \"1/1000\",\n" + + " \"start_pts\": 0,\n" + + " \"start_time\": \"0.000000\",\n" + + " \"bits_per_raw_sample\": \"8\",\n" + + " \"disposition\": {\n" + + " \"default\": 0,\n" + + " \"dub\": 0,\n" + + " \"original\": 0,\n" + + " \"comment\": 0,\n" + + " \"lyrics\": 0,\n" + + " \"karaoke\": 0,\n" + + " \"forced\": 0,\n" + + " \"hearing_impaired\": 0,\n" + + " \"visual_impaired\": 0,\n" + + " \"clean_effects\": 0,\n" + + " \"attached_pic\": 0\n" + + " }\n" + " },\n" + + " {\n" + + " \"index\": 1,\n" + + " \"codec_name\": \"aac\",\n" + + " \"codec_long_name\": \"AAC (Advanced Audio Coding)\",\n" + + " \"profile\": \"LC\",\n" + + " \"codec_type\": \"audio\",\n" + + " \"codec_time_base\": \"1/44100\",\n" + + " \"codec_tag_string\": \"[0][0][0][0]\",\n" + + " \"codec_tag\": \"0x0000\",\n" + + " \"sample_fmt\": \"fltp\",\n" + + " \"sample_rate\": \"48000\",\n" + + " \"channels\": 6,\n" + + " \"channel_layout\": \"5.1\",\n" + + " \"bits_per_sample\": 0,\n" + + " \"r_frame_rate\": \"0/0\",\n" + + " \"avg_frame_rate\": \"0/0\",\n" + + " \"time_base\": \"1/1000\",\n" + + " \"start_pts\": 3,\n" + + " \"start_time\": \"0.003000\",\n" + + " \"disposition\": {\n" + + " \"default\": 0,\n" + + " \"dub\": 0,\n" + + " \"original\": 0,\n" + + " \"comment\": 0,\n" + + " \"lyrics\": 0,\n" + + " \"karaoke\": 0,\n" + + " \"forced\": 0,\n" + + " \"hearing_impaired\": 0,\n" + + " \"visual_impaired\": 0,\n" + + " \"clean_effects\": 0,\n" + + " \"attached_pic\": 0\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"format\": {\n" + + " \"filename\": \"/Users/test/Downloads/big_buck_bunny_360p_1mb.flv\",\n" + + " \"nb_streams\": 2,\n" + + " \"nb_programs\": 0,\n" + + " \"format_name\": \"flv\",\n" + + " \"format_long_name\": \"FLV (Flash Video)\",\n" + + " \"start_time\": \"0.000000\",\n" + + " \"duration\": \"6.893000\",\n" + + " \"size\": \"1048720\",\n" + + " \"bit_rate\": \"1217142\",\n" + + " \"probe_score\": 100,\n" + + " \"tags\": {\n" + + " \"encoder\": \"Lavf53.24.2\"\n" + + " }\n" + + " }\n" + + "}"; - string = "Input #0, flv, from 'SampleVideo_360x240_1mb.flv':\n" + - " Metadata:\n" + " encoder : Lavf53.24.2\n" + - " Duration: 00:00:10.64, start: 0.000000, bitrate: 792 kb/s\n" + - " Stream #0:0: Audio: aac (LC), 48000 Hz, 5.1, fltp\n" + - " Stream #0:1: Video: flv1, yuv420p, 320x240, 25 fps, 25 tbr, 1k tbn"; info = FFmpegUtil.parseMediaInfo(file, string); assertEquals(file, info.file); - assertEquals( (10)*1000 + 64, info.durationMs); + assertEquals( 6893, info.durationMs); assertEquals(MediaContainer.FLV, info.container); - assertEquals(null, info.videoCodec); - assertEquals("320x240", info.videoResolution); - assertEquals(Integer.valueOf(692), info.videoBitrate); // This is guessed from total bitrate + assertEquals(VideoCodec.H264, info.videoCodec); + assertEquals("640x360", info.videoResolution); + assertEquals(Integer.valueOf(1117), info.videoBitrateK); // This is guessed from total bitrate assertEquals("25", info.videoFramerate); assertEquals(AudioCodec.AAC, info.audioCodec); assertEquals(Integer.valueOf(48000), info.audioSampleRate); - assertEquals(null, info.audioBitrate); + assertEquals(null, info.audioBitrateK); assertEquals(Integer.valueOf(2), info.audioChannels); - string = "Input #0, flv, from 'SampleVideo_360x240_1mb.flv':\n" + - " Metadata:\n" + " encoder : Lavf53.24.2\n" + - " Duration: 00:00:10.64, start: 0.000000, bitrate: 792 kb/s\n" + - " Stream #0:0: Audio: aac (LC), 48000 Hz, 5.1, fltp, 92 kb/s (default)\n" + - " Stream #0:1: Video: flv1, yuv420p, 320x240, 25 fps, 25 tbr, 1k tbn"; + + string = "{\n" + + " \"streams\": [\n" + + " {\n" + + " \"index\": 0,\n" + + " \"codec_name\": \"h264\",\n" + + " \"codec_long_name\": \"H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10\",\n" + + " \"profile\": \"Main\",\n" + + " \"codec_type\": \"video\",\n" + + " \"codec_time_base\": \"1/50\",\n" + + " \"codec_tag_string\": \"[0][0][0][0]\",\n" + + " \"codec_tag\": \"0x0000\",\n" + + " \"width\": 640,\n" + + " \"height\": 360,\n" + + " \"coded_width\": 640,\n" + + " \"coded_height\": 360,\n" + + " \"has_b_frames\": 0,\n" + + " \"sample_aspect_ratio\": \"1:1\",\n" + + " \"display_aspect_ratio\": \"16:9\",\n" + + " \"pix_fmt\": \"yuv420p\",\n" + + " \"level\": 30,\n" + + " \"chroma_location\": \"left\",\n" + + " \"refs\": 1,\n" + + " \"is_avc\": \"true\",\n" + + " \"nal_length_size\": \"4\",\n" + + " \"r_frame_rate\": \"25/1\",\n" + + " \"avg_frame_rate\": \"25/1\",\n" + + " \"time_base\": \"1/1000\",\n" + + " \"start_pts\": 0,\n" + + " \"start_time\": \"0.000000\",\n" + + " \"bits_per_raw_sample\": \"8\",\n" + + " \"disposition\": {\n" + + " \"default\": 0,\n" + + " \"dub\": 0,\n" + + " \"original\": 0,\n" + + " \"comment\": 0,\n" + + " \"lyrics\": 0,\n" + + " \"karaoke\": 0,\n" + + " \"forced\": 0,\n" + + " \"hearing_impaired\": 0,\n" + + " \"visual_impaired\": 0,\n" + + " \"clean_effects\": 0,\n" + + " \"attached_pic\": 0\n" + + " }\n" + " },\n" + + " {\n" + + " \"index\": 1,\n" + + " \"codec_name\": \"aac\",\n" + + " \"codec_long_name\": \"AAC (Advanced Audio Coding)\",\n" + + " \"profile\": \"LC\",\n" + + " \"codec_type\": \"audio\",\n" + + " \"codec_time_base\": \"1/44100\",\n" + + " \"codec_tag_string\": \"[0][0][0][0]\",\n" + + " \"codec_tag\": \"0x0000\",\n" + + " \"sample_fmt\": \"fltp\",\n" + + " \"sample_rate\": \"48000\",\n" + + " \"channels\": 6,\n" + + " \"channel_layout\": \"5.1\",\n" + + " \"bits_per_sample\": 0,\n" + + " \"r_frame_rate\": \"0/0\",\n" + + " \"avg_frame_rate\": \"0/0\",\n" + + " \"time_base\": \"1/1000\",\n" + + " \"start_pts\": 3,\n" + + " \"start_time\": \"0.003000\",\n" + + " \"bit_rate\": 517000,\n" + // Not actually from this file, just added here for the tests + " \"disposition\": {\n" + + " \"default\": 0,\n" + + " \"dub\": 0,\n" + + " \"original\": 0,\n" + + " \"comment\": 0,\n" + + " \"lyrics\": 0,\n" + + " \"karaoke\": 0,\n" + + " \"forced\": 0,\n" + + " \"hearing_impaired\": 0,\n" + + " \"visual_impaired\": 0,\n" + + " \"clean_effects\": 0,\n" + + " \"attached_pic\": 0\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"format\": {\n" + + " \"filename\": \"/Users/test/Downloads/big_buck_bunny_360p_1mb.flv\",\n" + + " \"nb_streams\": 2,\n" + + " \"nb_programs\": 0,\n" + + " \"format_name\": \"flv\",\n" + + " \"format_long_name\": \"FLV (Flash Video)\",\n" + + " \"start_time\": \"0.000000\",\n" + + " \"duration\": \"6.893000\",\n" + + " \"size\": \"1048720\",\n" + + " \"bit_rate\": \"1217142\",\n" + + " \"probe_score\": 100,\n" + + " \"tags\": {\n" + + " \"encoder\": \"Lavf53.24.2\"\n" + + " }\n" + + " }\n" + + "}"; info = FFmpegUtil.parseMediaInfo(file, string); - assertEquals(Integer.valueOf(700), info.videoBitrate); // This is guessed from total and audio bitrate + assertEquals(Integer.valueOf(700), info.videoBitrateK); // This is guessed from total and audio bitrate } @Test diff --git a/app/src/test/shell/assets/SampleVideo_360x240_1mb.flv b/app/src/test/shell/assets/SampleVideo_360x240_1mb.flv new file mode 100644 index 00000000..452ce443 Binary files /dev/null and b/app/src/test/shell/assets/SampleVideo_360x240_1mb.flv differ diff --git a/app/src/test/shell/assets/SampleVideo_360x240_1mb.mkv b/app/src/test/shell/assets/SampleVideo_360x240_1mb.mkv new file mode 100644 index 00000000..dc66dfc6 Binary files /dev/null and b/app/src/test/shell/assets/SampleVideo_360x240_1mb.mkv differ diff --git a/app/src/test/shell/assets/SampleVideo_360x240_1mb.mp4 b/app/src/test/shell/assets/SampleVideo_360x240_1mb.mp4 new file mode 100644 index 00000000..a203d0cd Binary files /dev/null and b/app/src/test/shell/assets/SampleVideo_360x240_1mb.mp4 differ diff --git a/app/src/test/shell/test.sh b/app/src/test/shell/test.sh new file mode 100755 index 00000000..ae0f3275 --- /dev/null +++ b/app/src/test/shell/test.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +ASSETS="$(dirname $0)/assets" + +encode_file() +{ + args="" + for arg in "$@" + do + args="${args} $arg" + done + + # Clear the logs, so we know when the encoding is complete + adb shell logcat -c + + echo "Encoding start: ${args}" + adb shell am start -a "protect.videotranscoder.ENCODE" --ez skipDialog true ${args} + + for i in `seq 1 300`; do + result=$(adb logcat -d | grep VideoTranscoder | grep "Encode result") + if [ ! -z "${result}" ]; then + echo "Encoding complete" + + if [ ! -z "$(echo ${result} | grep 'Encode result: false')" ]; then + echo "Encoding failed" + exit 1 + fi + return + fi + sleep 1 + done + + echo "Encoding did not complete before timeout" + exit 1 +} + +pull_file() +{ + adb pull "$1" . +} + +remove_file() +{ + adb shell rm "$1" +} + +push_asset() +{ + adb push "$ASSETS/$1" /sdcard +} + + +echo "test mp4 -> mp4" +FILE=SampleVideo_360x240_1mb.mp4 +OUTPUT=output.mp4 +push_asset ${FILE} +encode_file --es inputVideoFilePath "/sdcard/${FILE}" --es outputFilePath /sdcard/${OUTPUT} --es mediaContainer mp4 --es videoCodec h264 --ei videoBitrateK 2000 --es resolution 360x240 --es fps 19 --es audioCodec aac --ei audioSampleRate 22050 --ei audioBitrateK 100 --es audioChannel 2 +pull_file /sdcard/${OUTPUT} +remove_file /sdcard/${OUTPUT} +ffprobe -i ${OUTPUT} + +echo "test mp4 -> flv" +FILE=SampleVideo_360x240_1mb.mp4 +OUTPUT=output.flv +push_asset ${FILE} +encode_file --es inputVideoFilePath "/sdcard/${FILE}" --es outputFilePath /sdcard/${OUTPUT} --es mediaContainer flv --es videoCodec h264 --ei videoBitrateK 2000 --es resolution 360x240 --es fps 19 --es audioCodec aac --ei audioSampleRate 22050 --ei audioBitrateK 100 --es audioChannel 2 +pull_file /sdcard/${OUTPUT} +remove_file /sdcard/${OUTPUT} +ffprobe -i ${OUTPUT} + +echo "test mp4 -> mkv" +FILE=SampleVideo_360x240_1mb.mp4 +OUTPUT=output.mkv +push_asset ${FILE} +encode_file --es inputVideoFilePath "/sdcard/${FILE}" --es outputFilePath /sdcard/${OUTPUT} --es mediaContainer matroska --es videoCodec h264 --ei videoBitrateK 2000 --es resolution 360x240 --es fps 19 --es audioCodec aac --ei audioSampleRate 22050 --ei audioBitrateK 100 --es audioChannel 2 +pull_file /sdcard/${OUTPUT} +remove_file /sdcard/${OUTPUT} +ffprobe -i ${OUTPUT} + +echo "test mp4 -> gif" +FILE=SampleVideo_360x240_1mb.mp4 +OUTPUT=output.gif +push_asset ${FILE} +encode_file --es inputVideoFilePath "/sdcard/${FILE}" --es outputFilePath /sdcard/${OUTPUT} --es mediaContainer gif --es videoCodec gif --ei videoBitrateK 2000 --es resolution 360x240 --es fps 19 +pull_file /sdcard/${OUTPUT} +remove_file /sdcard/${OUTPUT} +ffprobe -i ${OUTPUT} + +echo "test mp4 -> mp3" +FILE=SampleVideo_360x240_1mb.mp4 +OUTPUT=output.mp3 +push_asset ${FILE} +encode_file --es inputVideoFilePath "/sdcard/${FILE}" --es outputFilePath /sdcard/${OUTPUT} --es mediaContainer mp3 --es audioCodec mp3 --ei audioSampleRate 22050 --ei audioBitrateK 100 --es audioChannel 2 +pull_file /sdcard/${OUTPUT} +remove_file /sdcard/${OUTPUT} +ffprobe -i ${OUTPUT} + +echo "test mp4 -> ogg" +FILE=SampleVideo_360x240_1mb.mp4 +OUTPUT=output.ogg +push_asset ${FILE} +encode_file --es inputVideoFilePath "/sdcard/${FILE}" --es outputFilePath /sdcard/${OUTPUT} --es mediaContainer ogg --es audioCodec vorbis --ei audioSampleRate 22050 --ei audioBitrateK 100 --es audioChannel 2 +pull_file /sdcard/${OUTPUT} +remove_file /sdcard/${OUTPUT} +ffprobe -i ${OUTPUT} + +echo "test mp4 -> opus" +FILE=SampleVideo_360x240_1mb.mp4 +OUTPUT=output.opus +push_asset ${FILE} +encode_file --es inputVideoFilePath "/sdcard/${FILE}" --es outputFilePath /sdcard/${OUTPUT} --es mediaContainer opus --es audioCodec libopus --ei audioSampleRate 48000 --ei audioBitrateK 100 --es audioChannel 2 +pull_file /sdcard/${OUTPUT} +remove_file /sdcard/${OUTPUT} +ffprobe -i ${OUTPUT}