diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..a79483bd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +## 0.3.0 +- Fix cancel() sometimes not working. (Thanks @strayerM and @PinkFloyded) +- Geolocation support on API>=19. (Thanks @hkurokawa) + +## 0.2.0 +- Experimental audio transcoding support. (Thanks @aaron112) +- Fix transcode does not run on Huawei Ascend P7. (Thanks @spiritedRunning) +- Fix race condition caused by not closing output before callback. (Thanks @ryanwilliams83) + +## 0.1.10 +- `Future` support. (Thanks @MaiKambayashi) + +## 0.1.X +- Stability updates. (Thanks @ozyozyo) + +## 0.1.0 +- First release. diff --git a/README.md b/README.md index df1bb947..2d72b83d 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ repositories { ``` ```groovy -compile 'net.ypresto.androidtranscoder:android-transcoder:0.1.10' +compile 'net.ypresto.androidtranscoder:android-transcoder:0.2.0' ``` ## Note (PLEASE READ FIRST) @@ -78,6 +78,20 @@ Use [qtfaststart-java](https://github.com/ypresto/qtfaststart-java) to place moo - Android does not gurantees that all devices have bug-free codecs/accelerators for your codec parameters (especially, resolution). Refer [supported media formats](http://developer.android.com/guide/appendix/media-formats.html) for parameters guaranteed by [CTS](https://source.android.com/compatibility/cts-intro.html). - This library does not support video files recorded by other device like digital cameras, iOS (mov files, including non-baseline profile h.264), etc. + +## More information about internals + +There is a blog post about this library written in Japanese. +http://qiita.com/yuya_presto/items/d48e29c89109b746d000 + +While it is Japanese, diagrams would be useful for understanding internals of this library. + +## References for Android Low-Level Media APIs + +- http://bigflake.com/mediacodec/ +- https://github.com/google/grafika +- https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright + ## License ``` @@ -95,9 +109,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` - -## References for Android Low-Level Media APIs - -- http://bigflake.com/mediacodec/ -- https://github.com/google/grafika -- https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright diff --git a/build.gradle b/build.gradle index e220f0b8..13089606 100644 --- a/build.gradle +++ b/build.gradle @@ -3,9 +3,13 @@ buildscript { repositories { jcenter() + maven { + url 'https://maven.google.com/' + name 'Google' + } } dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.3.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -15,5 +19,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url 'https://maven.google.com/' + name 'Google' + } } } diff --git a/example/build.gradle b/example/build.gradle index e0179e52..8560676b 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' android { compileSdkVersion 24 - buildToolsVersion "24.0.1" + buildToolsVersion '25.0.0' defaultConfig { applicationId "net.ypresto.androidtranscoder.example" @@ -21,4 +21,5 @@ android { dependencies { compile project(':lib') + compile 'com.android.support:support-core-utils:24.2.0' } diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 19c33591..498d9288 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -1,21 +1,35 @@ + package="net.ypresto.androidtranscoder.example"> + + + android:theme="@style/AppTheme"> + android:label="@string/app_name"> + + + + diff --git a/example/src/main/java/net/ypresto/androidtranscoder/example/TranscoderActivity.java b/example/src/main/java/net/ypresto/androidtranscoder/example/TranscoderActivity.java index f67f1543..7e08566b 100644 --- a/example/src/main/java/net/ypresto/androidtranscoder/example/TranscoderActivity.java +++ b/example/src/main/java/net/ypresto/androidtranscoder/example/TranscoderActivity.java @@ -7,6 +7,7 @@ import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.SystemClock; +import android.support.v4.content.FileProvider; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -26,6 +27,7 @@ public class TranscoderActivity extends Activity { private static final String TAG = "TranscoderActivity"; + private static final String FILE_PROVIDER_AUTHORITY = "net.ypresto.androidtranscoder.example.fileprovider"; private static final int REQUEST_CODE_PICK = 1; private static final int PROGRESS_BAR_MAX = 1000; private Future mFuture; @@ -55,7 +57,10 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { final File file; if (resultCode == RESULT_OK) { try { - file = File.createTempFile("transcode_test", ".mp4", getExternalFilesDir(null)); + File outputDir = new File(getExternalFilesDir(null), "outputs"); + //noinspection ResultOfMethodCallIgnored + outputDir.mkdir(); + file = File.createTempFile("transcode_test", ".mp4", outputDir); } catch (IOException e) { Log.e(TAG, "Failed to create temporary file.", e); Toast.makeText(this, "Failed to create temporary file.", Toast.LENGTH_LONG).show(); @@ -89,7 +94,10 @@ public void onTranscodeProgress(double progress) { public void onTranscodeCompleted() { Log.d(TAG, "transcoding took " + (SystemClock.uptimeMillis() - startTime) + "ms"); onTranscodeFinished(true, "transcoded file placed on " + file, parcelFileDescriptor); - startActivity(new Intent(Intent.ACTION_VIEW).setDataAndType(Uri.fromFile(file), "video/mp4")); + Uri uri = FileProvider.getUriForFile(TranscoderActivity.this, FILE_PROVIDER_AUTHORITY, file); + startActivity(new Intent(Intent.ACTION_VIEW) + .setDataAndType(uri, "video/mp4") + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)); } @Override diff --git a/example/src/main/res/xml/file_paths.xml b/example/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..5b254ecc --- /dev/null +++ b/example/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d14f516d..9555d195 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Mar 09 14:43:12 JST 2015 +#Thu Jun 08 12:40:11 IST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/lib/build.gradle b/lib/build.gradle index dfc02d9e..1ab6c7ba 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -12,7 +12,7 @@ apply plugin: 'bintray-release' android { compileSdkVersion 24 - buildToolsVersion "24.0.1" + buildToolsVersion '25.0.2' defaultConfig { minSdkVersion 18 @@ -32,7 +32,7 @@ android { publish { groupId = 'net.ypresto.androidtranscoder' artifactId = 'android-transcoder' - version = '0.1.10-SNAPSHOT' + version = '0.3.0' licences = ['Apache-2.0'] website = 'https://github.com/ypresto/android-transcoder' autoPublish = false diff --git a/lib/src/androidTest/java/net/ypresto/androidtranscoder/utils/ISO6709LocationParserTest.java b/lib/src/androidTest/java/net/ypresto/androidtranscoder/utils/ISO6709LocationParserTest.java new file mode 100644 index 00000000..a1a9caae --- /dev/null +++ b/lib/src/androidTest/java/net/ypresto/androidtranscoder/utils/ISO6709LocationParserTest.java @@ -0,0 +1,35 @@ +package net.ypresto.androidtranscoder.utils; + +import junit.framework.TestCase; + +public class ISO6709LocationParserTest extends TestCase { + public void testParse() { + ISO6709LocationParser parser = new ISO6709LocationParser(); + assertEquals(new float[]{35.658632f, 139.745411f}, parser.parse("+35.658632+139.745411/")); + assertEquals(new float[]{40.75f, -074.00f}, parser.parse("+40.75-074.00/")); + // with Altitude + assertEquals(new float[]{-90f, +0f}, parser.parse("-90+000+2800/")); + assertEquals(new float[]{27.5916f, 086.5640f}, parser.parse("+27.5916+086.5640+8850/")); + // ranged data + assertEquals(new float[]{35.331f, 134.224f}, parser.parse("+35.331+134.224/+35.336+134.228/")); + assertEquals(new float[]{35.331f, 134.224f}, parser.parse("+35.331+134.224/+35.336+134.228/+35.333+134.229/+35.333+134.227/")); + } + + public void testParseFailure() { + ISO6709LocationParser parser = new ISO6709LocationParser(); + assertNull(parser.parse(null)); + assertNull(parser.parse("")); + assertNull(parser.parse("35 deg 65' 86.32\" N, 139 deg 74' 54.11\" E")); + assertNull(parser.parse("+35.658632")); + assertNull(parser.parse("+35.658632-")); + assertNull(parser.parse("40.75-074.00")); + assertNull(parser.parse("+40.75-074.00.00")); + } + + private static void assertEquals(float[] expected, float[] actual) { + assertEquals(expected.length, actual.length); + for (int i = 0; i < expected.length; i++) { + assertTrue(Float.compare(expected[i], actual[i]) == 0); + } + } +} \ No newline at end of file diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/MediaTranscoder.java b/lib/src/main/java/net/ypresto/androidtranscoder/MediaTranscoder.java index 3a758677..7ec32bc9 100644 --- a/lib/src/main/java/net/ypresto/androidtranscoder/MediaTranscoder.java +++ b/lib/src/main/java/net/ypresto/androidtranscoder/MediaTranscoder.java @@ -123,20 +123,20 @@ public void onTranscodeProgress(double progress) { @Override public void onTranscodeCompleted() { - listener.onTranscodeCompleted(); closeStream(); + listener.onTranscodeCompleted(); } @Override public void onTranscodeCanceled() { - listener.onTranscodeCanceled(); closeStream(); + listener.onTranscodeCanceled(); } @Override public void onTranscodeFailed(Exception exception) { - listener.onTranscodeFailed(exception); closeStream(); + listener.onTranscodeFailed(exception); } private void closeStream() { diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/engine/AudioTrackTranscoder.java b/lib/src/main/java/net/ypresto/androidtranscoder/engine/AudioTrackTranscoder.java index 47e0a61d..99034ef0 100644 --- a/lib/src/main/java/net/ypresto/androidtranscoder/engine/AudioTrackTranscoder.java +++ b/lib/src/main/java/net/ypresto/androidtranscoder/engine/AudioTrackTranscoder.java @@ -79,7 +79,7 @@ public void setup() { @Override public MediaFormat getDeterminedFormat() { - return mInputFormat; + return mActualOutputFormat; } @Override diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaFormatValidator.java b/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaFormatValidator.java index 67ea5ba8..6182ee60 100644 --- a/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaFormatValidator.java +++ b/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaFormatValidator.java @@ -18,14 +18,8 @@ import android.media.MediaFormat; import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants; -import net.ypresto.androidtranscoder.utils.AvcCsdUtils; -import net.ypresto.androidtranscoder.utils.AvcSpsUtils; - -import java.nio.ByteBuffer; class MediaFormatValidator { - // Refer: http://en.wikipedia.org/wiki/H.264/MPEG-4_AVC#Profiles - private static final byte PROFILE_IDC_BASELINE = 66; public static void validateVideoOutputFormat(MediaFormat format) { String mime = format.getString(MediaFormat.KEY_MIME); @@ -34,11 +28,6 @@ public static void validateVideoOutputFormat(MediaFormat format) { if (!MediaFormatExtraConstants.MIMETYPE_VIDEO_AVC.equals(mime)) { throw new InvalidOutputFormatException("Video codecs other than AVC is not supported, actual mime type: " + mime); } - ByteBuffer spsBuffer = AvcCsdUtils.getSpsBuffer(format); - byte profileIdc = AvcSpsUtils.getProfileIdc(spsBuffer); - if (profileIdc != PROFILE_IDC_BASELINE) { - throw new InvalidOutputFormatException("Non-baseline AVC video profile is not supported by Android OS, actual profile_idc: " + profileIdc); - } } public static void validateAudioOutputFormat(MediaFormat format) { diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java b/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java index c883979c..812c31d4 100644 --- a/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java +++ b/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java @@ -19,9 +19,12 @@ import android.media.MediaFormat; import android.media.MediaMetadataRetriever; import android.media.MediaMuxer; +import android.os.Build; import android.util.Log; +import net.ypresto.androidtranscoder.BuildConfig; import net.ypresto.androidtranscoder.format.MediaFormatStrategy; +import net.ypresto.androidtranscoder.utils.ISO6709LocationParser; import net.ypresto.androidtranscoder.utils.MediaExtractorUtils; import java.io.FileDescriptor; @@ -137,9 +140,17 @@ private void setupMetadata() throws IOException { // skip } - // TODO: parse ISO 6709 - // String locationString = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION); - // mMuxer.setLocation(Integer.getInteger(rotationString, 0)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + String locationString = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION); + if (locationString != null) { + float[] location = new ISO6709LocationParser().parse(locationString); + if (location != null) { + mMuxer.setLocation(location[0], location[1]); + } else { + Log.d(TAG, "Failed to parse the location metadata: " + locationString); + } + } + } try { mDurationUs = Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) * 1000; @@ -189,7 +200,7 @@ public void onDetermineOutputFormat() { mAudioTrackTranscoder.setup(); } - private void runPipelines() { + private void runPipelines() throws InterruptedException { long loopCount = 0; if (mDurationUs <= 0) { double progress = PROGRESS_UNKNOWN; @@ -208,11 +219,7 @@ private void runPipelines() { if (mProgressCallback != null) mProgressCallback.onProgress(progress); } if (!stepped) { - try { - Thread.sleep(SLEEP_TO_WAIT_TRACK_TRANSCODERS); - } catch (InterruptedException e) { - // nothing to do - } + Thread.sleep(SLEEP_TO_WAIT_TRACK_TRANSCODERS); } } } diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/format/MediaFormatStrategyPresets.java b/lib/src/main/java/net/ypresto/androidtranscoder/format/MediaFormatStrategyPresets.java index 99fc9b90..30802586 100644 --- a/lib/src/main/java/net/ypresto/androidtranscoder/format/MediaFormatStrategyPresets.java +++ b/lib/src/main/java/net/ypresto/androidtranscoder/format/MediaFormatStrategyPresets.java @@ -16,6 +16,9 @@ package net.ypresto.androidtranscoder.format; public class MediaFormatStrategyPresets { + public static final int AUDIO_BITRATE_AS_IS = -1; + public static final int AUDIO_CHANNELS_AS_IS = -1; + /** * @deprecated Use {@link #createExportPreset960x540Strategy()}. */ @@ -45,7 +48,7 @@ public static MediaFormatStrategy createAndroid720pStrategy(int bitrate) { /** * Preset based on Nexus 4 camera recording with 720p quality. * This preset is ensured to work on any Android >=4.3 devices by Android CTS (if codec is available). - *

+ *
* Note: audio transcoding is experimental feature. * * @param bitrate Preferred bitrate for video encoding. diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/utils/AvcCsdUtils.java b/lib/src/main/java/net/ypresto/androidtranscoder/utils/AvcCsdUtils.java index d458f66c..049296b9 100644 --- a/lib/src/main/java/net/ypresto/androidtranscoder/utils/AvcCsdUtils.java +++ b/lib/src/main/java/net/ypresto/androidtranscoder/utils/AvcCsdUtils.java @@ -29,6 +29,9 @@ public class AvcCsdUtils { private static final byte[] AVC_START_CODE_4 = {0x00, 0x00, 0x00, 0x01}; // Refer: http://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set/ private static final byte AVC_SPS_NAL = 103; // 0<<7 + 3<<5 + 7<<0 + // https://tools.ietf.org/html/rfc6184 + private static final byte AVC_SPS_NAL_2 = 39; // 0<<7 + 1<<5 + 7<<0 + private static final byte AVC_SPS_NAL_3 = 71; // 0<<7 + 2<<5 + 7<<0 /** * @return ByteBuffer contains SPS without NAL header. @@ -40,9 +43,12 @@ public static ByteBuffer getSpsBuffer(MediaFormat format) { prefixedSpsBuffer.flip(); skipStartCode(prefixedSpsBuffer); - if (prefixedSpsBuffer.get() != AVC_SPS_NAL) { + + byte spsNalData = prefixedSpsBuffer.get(); + if (spsNalData != AVC_SPS_NAL && spsNalData != AVC_SPS_NAL_2 && spsNalData != AVC_SPS_NAL_3) { throw new IllegalStateException("Got non SPS NAL data."); } + return prefixedSpsBuffer.slice(); } diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/utils/ISO6709LocationParser.java b/lib/src/main/java/net/ypresto/androidtranscoder/utils/ISO6709LocationParser.java new file mode 100644 index 00000000..273007d3 --- /dev/null +++ b/lib/src/main/java/net/ypresto/androidtranscoder/utils/ISO6709LocationParser.java @@ -0,0 +1,37 @@ +package net.ypresto.androidtranscoder.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ISO6709LocationParser { + private final Pattern pattern; + + public ISO6709LocationParser() { + this.pattern = Pattern.compile("([+\\-][0-9.]+)([+\\-][0-9.]+)"); + } + + /** + * This method parses the given string representing a geographic point location by coordinates in ISO 6709 format + * and returns the latitude and the longitude in float. If location is not in ISO 6709 format, + * this method returns null + * + * @param location a String representing a geographic point location by coordinates in ISO 6709 format + * @return null if the given string is not as expected, an array of floats with size 2, + * where the first element represents latitude and the second represents longitude, otherwise. + */ + public float[] parse(String location) { + if (location == null) return null; + Matcher m = pattern.matcher(location); + if (m.find() && m.groupCount() == 2) { + String latstr = m.group(1); + String lonstr = m.group(2); + try { + float lat = Float.parseFloat(latstr); + float lon = Float.parseFloat(lonstr); + return new float[]{lat, lon}; + } catch (NumberFormatException ignored) { + } + } + return null; + } +} diff --git a/lib/src/main/res/values/strings.xml b/lib/src/main/res/values/strings.xml deleted file mode 100644 index 85420055..00000000 --- a/lib/src/main/res/values/strings.xml +++ /dev/null @@ -1,2 +0,0 @@ - -