I'm looking for the alternate of Movie like to get the duration of GIF. I tried in imageDecoder but I can't able to get the duration.
//Deprecated
val movie = Movie.decodeStream(`is`)
val duration = movie.duration()
Movie probably still works even though it's deprecated. But if you're not going to go on to use that Movie instance to play the GIF, that's a bad way of getting the duration because it will have to load the entire thing when all you really need to find the duration is in the meta data at the beginning of the file.
You could use the Metadata Extractor library to do this.
Since it's reading from a file, it is blocking and should be done in the background. Here's an example using a suspend function to accomplish that.
/** Returns duration in ms of the GIF of the stream, 0 if it has no duration,
* or null if it could not be read. */
suspend fun InputStream.readGifDurationOrNull(): Int? = withContext(Dispatchers.IO) {
try {
val metadata = ImageMetadataReader.readMetadata(this#readGifDurationOrNull)
val gifControlDirectories = metadata.getDirectoriesOfType(GifControlDirectory::class.java)
if (gifControlDirectories.size <= 1) {
return#withContext 0
}
gifControlDirectories.sumOf {
it.getInt(GifControlDirectory.TAG_DELAY) * 10 // Gif uses 10ms units
}
} catch (e: Exception) {
Log.e("readGifDurationOrNull", "Could not read metadata from input", e)
null
}
}
Credit to this answer for how to get the appropriate duration info from the metadata.
I am currently trying to implement a MediaBrowserService to build a media app for Android Auto.
I followed the official Android Auto documentation (https://developer.android.com/training/cars/media#onLoadChildren) to implement theonLoadChildren function.
Following is a code snippet that I tried to show the content on the Android Auto screen:
override fun onLoadChildren(parentId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
...
if (parentId == NODE_LIBRARY_ALBUMS) {
val items = mutableListOf<MediaBrowserCompat.MediaItem>()
val albumList = LibraryManager.getAlbumList()
for (it in albumList) {
val descriptionBuilder = MediaDescriptionCompat.Builder()
.setTitle(it.albumName)
items.add(MediaBrowserCompat.MediaItem(descriptionBuilder.build(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE))
}
result.sendResult(items)
}
...
}
This works pretty well, when the number of items is small enough.
However, when the number of items is large (e.g., about 5,000 items), the following error appears:
E/JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = 1339384)
I found that several other media apps (e.g., Samsung Music) that support Android Auto can show a large number of items.
Is there any way to return a large number of items on the onLoadChildren function, or is there any other way to solve this issue?
Thanks!
Probably you have to split the large data into a small pieces. For example, you have a list of 5000 items. So inside you MediaBrowserService in onLoadChildren do something like this
public fun onLoadChildren(parentId: String, result: Result<List<MediaBrowserCompat.MediaItem>>) {
if (MEDIA_ID_ROOT == parentId || itemsList.size > 100) {
fillMediaBrowsableResult(parentId, result);
}
else {
fillMediaPlayableResult(parentId, result);
}
}
//Split the large number of content to a parts
private fun fillMediaBrowsableResult(parentId: String, result: Result<List<MediaBrowserCompat.MediaItem>>) {
// Detect count of parts
val partsCount = itemsList.size / 100
if(itemsList.size % 100 > 0){
partsCount ++
}
val mediaItems = mutableListOf<MediaBrowserCompat.MediaItem>()
//Create parts in a loop
for(i in 0 until partsCount){
val mediaDescription = MediaDescriptionCompat.Builder()
.setMediaId(i) // This is your next parent in onLoadChildren when you click on it in AA
.setTitle("Part ${i + 1}")
.build();
val mediaItem =. MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
mediaItems.add(mediaItem)
}
result.sendResult(mediaItems)
}
private fun fillMediaPlayableResult(parentId: String, result: Result<List<MediaBrowserCompat.MediaItem>>){
val intParent = parentId.toInt()
val startPosition = intParent * 100 // where to start for this part
val stopPosition = (intParent + 1) * 100 // where to finish for this part
if(stopPosition > itemsList.size){
stopPosition = itemsList.size
}
val mediaItems = mutableListOf<MediaBrowserCompat.MediaItem>()
for(i in startPosition..stopPosition){
//Generate playable content for this part
val item = itemsList[i]
val mediaDescription = MediaDescriptionCompat.Builder()
.setMediaId(item.id)
.setTitle(item.albumTitle)
.build();
val mediaItem =. MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
mediaItems.add(mediaItem)
}
result.sendResult(mediaItems)
}
I didn't check this code, but I think the idea is clear.
If you look into Android SDK document:
Note: Android Auto and Android Automotive OS have strict limits on how
many media items they can display in each level of the menu. These
limits minimize distractions for drivers and help them operate your
app with voice commands. ...
So, I think the best approach is to limit # of media items in UX design. For what you saw from the other apps working with lots of media items, they might used Jetpack Media 2 for pagination.
I'm currently creating a fitniss app in Android studio and for every exercise
there is a video loop playing in a VideoView
Now my problem is that the loop is not seamless there is a short break after every play. The videos cant be local because the app would get to big for the playstore
My Code in Kotlin:
private fun setExerciseVideo(){
vvVideo.visibility = View.VISIBLE
val uri: Uri = Uri.parse(exerciseList![currentExercisePosition+1].getVideo())
vvVideo.setVideoURI(uri)
vvVideo.start()
vvVideo.setOnPreparedListener {
mMediaPlayer = it
mMediaPlayer!!.isLooping = true
}
}
The code above looks good, but the issue might be the order the code is executed.
What happens if you move .setVideoURI(uri) .start() after the setOnPreparedListener?
If the suggestion above does not work you can try this also:
val uri: Uri = Uri.parse(exerciseList![currentExercisePosition+1].getVideo())
vvVideo.setVideoURI(uri)
vvVideo.setOnPreparedListener {
mMediaPlayer = it
mMediaPlayer!!.isLooping = true
mMediaPlayer!!.start()
}
Background
Over the past few days, I've worked on making a customizable, more updated version of a library for video trimming, here (based on this library)
The problem
While for the most part, I've succeeded making it customizable and even converted all files into Kotlin, it had a major issue with the trimming itself.
It assumes the input is always a File, so if the user chooses an item from the apps chooser that returns a Uri, it crashes. The reason for this is not just the UI itself, but also because a library that it uses for trimming (mp4parser) assumes an input of only File (or filepath) and not a Uri (wrote about it here). I tried multiple ways to let it get a Uri instead, but failed. Also wrote about it here.
That's why I used a solution that I've found on StackOverflow (here)for the trimming itself. The good thing about it is that it's quiet short and uses just Android's framework itself. However, it seems that for some video files, it always fails to trim them. As an example of such files, there is one on the original library repository, here (issue reported here).
Looking at the exception, this is what I got:
E: Unsupported mime 'audio/ac3'
E: FATAL EXCEPTION: pool-1-thread-1
Process: life.knowledge4.videocroppersample, PID: 26274
java.lang.IllegalStateException: Failed to add the track to the muxer
at android.media.MediaMuxer.nativeAddTrack(Native Method)
at android.media.MediaMuxer.addTrack(MediaMuxer.java:626)
at life.knowledge4.videotrimmer.utils.TrimVideoUtils.genVideoUsingMuxer(TrimVideoUtils.kt:77)
at life.knowledge4.videotrimmer.utils.TrimVideoUtils.genVideoUsingMp4Parser(TrimVideoUtils.kt:144)
at life.knowledge4.videotrimmer.utils.TrimVideoUtils.startTrim(TrimVideoUtils.kt:47)
at life.knowledge4.videotrimmer.BaseVideoTrimmerView$initiateTrimming$1.execute(BaseVideoTrimmerView.kt:220)
at life.knowledge4.videotrimmer.utils.BackgroundExecutor$Task.run(BackgroundExecutor.java:210)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:458)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:764)
What I've found
Reported about the issue here. I don't think it will get an answer, as the library hasn't updated in years...
Looking at the exception, I tried to also trim without sound. This works, but it's not a good thing, because we want to trim normally.
Thinking that this code might be based on someone else's code, I tried to find the original one. I've found that it is based on some old Google code on its gallery app, here, in a class called "VideoUtils.java" in package of "Gallery3d". Sadly, I don't see any new version for it. Latest one that I see is of Gingerbread, here.
The code that I've made out of it looks as such:
object TrimVideoUtils {
private const val DEFAULT_BUFFER_SIZE = 1024 * 1024
#JvmStatic
#WorkerThread
fun startTrim(context: Context, src: Uri, dst: File, startMs: Long, endMs: Long, callback: VideoTrimmingListener) {
dst.parentFile.mkdirs()
//Log.d(TAG, "Generated file path " + filePath);
val succeeded = genVideoUsingMuxer(context, src, dst.absolutePath, startMs, endMs, true, true)
Handler(Looper.getMainLooper()).post { callback.onFinishedTrimming(if (succeeded) Uri.parse(dst.toString()) else null) }
}
//https://stackoverflow.com/a/44653626/878126 https://android.googlesource.com/platform/packages/apps/Gallery2/+/634248d/src/com/android/gallery3d/app/VideoUtils.java
#JvmStatic
#WorkerThread
private fun genVideoUsingMuxer(context: Context, uri: Uri, dstPath: String, startMs: Long, endMs: Long, useAudio: Boolean, useVideo: Boolean): Boolean {
// Set up MediaExtractor to read from the source.
val extractor = MediaExtractor()
// val isRawResId=uri.scheme == "android.resource" && uri.host == context.packageName && !uri.pathSegments.isNullOrEmpty())
val fileDescriptor = context.contentResolver.openFileDescriptor(uri, "r")!!.fileDescriptor
extractor.setDataSource(fileDescriptor)
val trackCount = extractor.trackCount
// Set up MediaMuxer for the destination.
val muxer = MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
// Set up the tracks and retrieve the max buffer size for selected tracks.
val indexMap = SparseIntArray(trackCount)
var bufferSize = -1
try {
for (i in 0 until trackCount) {
val format = extractor.getTrackFormat(i)
val mime = format.getString(MediaFormat.KEY_MIME)
var selectCurrentTrack = false
if (mime.startsWith("audio/") && useAudio) {
selectCurrentTrack = true
} else if (mime.startsWith("video/") && useVideo) {
selectCurrentTrack = true
}
if (selectCurrentTrack) {
extractor.selectTrack(i)
val dstIndex = muxer.addTrack(format)
indexMap.put(i, dstIndex)
if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
val newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
bufferSize = if (newSize > bufferSize) newSize else bufferSize
}
}
}
if (bufferSize < 0)
bufferSize = DEFAULT_BUFFER_SIZE
// Set up the orientation and starting time for extractor.
val retrieverSrc = MediaMetadataRetriever()
retrieverSrc.setDataSource(fileDescriptor)
val degreesString = retrieverSrc.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
if (degreesString != null) {
val degrees = Integer.parseInt(degreesString)
if (degrees >= 0)
muxer.setOrientationHint(degrees)
}
if (startMs > 0)
extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
// Copy the samples from MediaExtractor to MediaMuxer. We will loop
// for copying each sample and stop when we get to the end of the source
// file or exceed the end time of the trimming.
val offset = 0
var trackIndex: Int
val dstBuf = ByteBuffer.allocate(bufferSize)
val bufferInfo = MediaCodec.BufferInfo()
// try {
muxer.start()
while (true) {
bufferInfo.offset = offset
bufferInfo.size = extractor.readSampleData(dstBuf, offset)
if (bufferInfo.size < 0) {
//InstabugSDKLogger.d(TAG, "Saw input EOS.");
bufferInfo.size = 0
break
} else {
bufferInfo.presentationTimeUs = extractor.sampleTime
if (endMs > 0 && bufferInfo.presentationTimeUs > endMs * 1000) {
//InstabugSDKLogger.d(TAG, "The current sample is over the trim end time.");
break
} else {
bufferInfo.flags = extractor.sampleFlags
trackIndex = extractor.sampleTrackIndex
muxer.writeSampleData(indexMap.get(trackIndex), dstBuf,
bufferInfo)
extractor.advance()
}
}
}
muxer.stop()
return true
// } catch (e: IllegalStateException) {
// Swallow the exception due to malformed source.
//InstabugSDKLogger.w(TAG, "The source video file is malformed");
} catch (e: Exception) {
e.printStackTrace()
} finally {
muxer.release()
}
return false
}
}
The exception is thrown on val dstIndex = muxer.addTrack(format) . For now, I've wrapped it in try-catch, to avoid a real crash.
I tried to search for newer versions of this code (assuming that it got fixed later), but failed.
Searching on the Internet and here, I've found only one similar question, here, but it's not the same at all.
The questions
Is it possible to use Android's framework to trim such problematic files? Maybe there is a newer version of the trimming of the videos code? I'm interested of course only for the pure implementation of video trimming, like the function I wrote above, of "genVideoUsingMuxer" .
As a temporary solution, is it possible to detect problematic input videos, so that I won't let the user start to trim them, as I know they will fail?
Is there maybe another alternative to both of those, that have a permissive license and doesn't bloat the app? For mp4parser, I wrote a separate question, here.
Why does it occur?
audio/ac3 is an unsupported mime type.
MediaMuxer.addTrack() (native) calls MPEG4Writer.addSource(), which prints this log message before returning an error.
EDIT
My aim was not to provide an answer to each of your sub-questions, but to give you some insight into the fundamental problem. The library you have chosen relies on the Android's MediaMuxer component. For whatever reason, the MediaMuxer developers did not add support for this particular audio format. We know this because the software prints out an explicit message to that effect, then immediately throws the IllegalStateException mentioned in your question.
Because the issue only involves a particular audio format, when you provide a video-only input, everything works fine.
To fix the problem, you can either alter the library to provide for the missing functionality, or find a new library that better suits your needs. sannies/mp4parser may be one such alternative, although it has different limitations (if I recall correctly, it requires all media to be in RAM during the mastering process). I do not know if it supports ac3 explicitly, but it should provide a framework to which you can add support for arbitrary mime types.
I would encourage you to wait for a more complete answer. There may be far better ways to do what you are trying to do. But it is apparent that the library you are using simply does not support all possible mime types.
Device - Nexus One
OS - Android 2.3.4
Class - CameraRoll
Method - addBitmapData()
Error - [ErrorEvent type="error" bubbles=false
cancelable=false eventPhase=2 text="Error #2038: File I/O Error."
errorID=2038]
I'm trying to develop a photo app but am having problems saving the full sized version of the image back to the CameraRoll. This is very frustrating as I've only seen examples saving the stage to CameraRoll (which I can get to work).
Is there a limitation to saving back to CameraRoll?? When I try to load an image (2592 x 1944) and save it directly back to CameraRoll using addBitmapData(), I get the following error.
[ErrorEvent type="error" bubbles=false cancelable=false eventPhase=2 text="Error #2038: File I/O Error." errorID=2038]
Here's a code sample.
// class vars for CameraRoll and Loader
private var _cameraRoll:CameraRoll = new CameraRoll();
private var _loader:Loader = new Loader();
// launch _cameraRoll
private function launchCameraRoll(e:MouseEvent):void {
_cameraRoll.addEventListener(MediaEvent.SELECT, loadImg);
_cameraRoll.browseForImage();
}
// open selected image using _loader
private function loadImg(e:MediaEvent):void {
if (e.data.isAsync) {
_loader.contentLoaderInfo.addEventListener(Event.COMPLETE, saveImage);
_loader.loadFilePromise(e.data);
} else {
_loader.loadFilePromise(e.data);
saveImage();
}
}
// once loaded, save image immediately back to _cameraRoll
private function saveImage(e:Event = null):void {
_cameraRoll.addEventListener(ErrorEvent.ERROR, onError);
_cameraRoll.addEventListener(Event.COMPLETE, onComplete);
var bmd:BitmapData = new BitmapData(_loader.width, _loader.height);
bmd.draw(_loader);
_cameraRoll.addBitmapData(bmd);
}
// trace error
private function onError(e:ErrorEvent):void {
trace(e); // [ErrorEvent type="error" bubbles=false cancelable=false eventPhase=2 text="Error #2038: File I/O Error." errorID=2038]
}
// show complete status
private function onComplete(e:Event):void {
trace("complete");
}
I had the same problem (even with real small images).
I solved it by uncommenting this line in the <android><manifestAdditions> section of the [Application]-app.xml file:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
have you tried to save image in lower res? e.g. 1024x768? Maybe there is a limitation on the size that your device supports, I've found this or maybe the disk space is problem like mentioned here
best regards