All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowMediaMuxer Maven / Gradle / Ivy

package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.O;

import android.annotation.NonNull;
import android.media.MediaCodec;
import android.media.MediaMuxer;
import android.media.MediaMuxer.Format;
import dalvik.system.CloseGuard;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.util.ReflectionHelpers;

/**
 * Implementation of {@link android.media.MediaMuxer} which directly passes input bytes to the
 * specified file, with no modification.
 */
@Implements(value = MediaMuxer.class, minSdk = LOLLIPOP)
public class ShadowMediaMuxer {
  // Maps between 'native' ids and corresponding output streams.
  private static final ConcurrentHashMap outputStreams =
      new ConcurrentHashMap<>();

  // Maps between 'native' ids and AtomicInteger objects tracking next track indices.
  private static final ConcurrentHashMap nextTrackIndices =
      new ConcurrentHashMap<>();

  // Maps between file descriptors and their original output stream.
  private static final ConcurrentHashMap fdToStream =
      new ConcurrentHashMap<>();

  private static final Random random = new Random();

  // Keep in sync with MediaMuxer.java.
  private static final int MUXER_STATE_INITIALIZED = 0;

  @RealObject private MediaMuxer realMuxer;

  /**
   * Opens a FileOutputStream for the given path, and sets muxer state.
   *
   * 

This needs to be shadowed, because the current MediaMuxer constructor opens a * RandomAccessFile, passes only the FileDescriptor along, and then closes the file. Since there * does not appear to be an easy way to go from FileDescriptor to a writeable stream in Java, this * method overrides that behavior to instead open and maintain a FileOutputStream. */ @Implementation protected void __constructor__(@NonNull String path, @Format int format) throws IOException { if (path == null) { throw new IllegalArgumentException("path must not be null"); } // Create a stream, and cache a mapping from file descriptor to stream. FileOutputStream stream = new FileOutputStream(path); FileDescriptor fd = stream.getFD(); fdToStream.put(fd, stream); // Initialize the CloseGuard and last track index, since they are otherwise null and 0. CloseGuard guard = CloseGuard.get(); ReflectionHelpers.setField(MediaMuxer.class, realMuxer, "mCloseGuard", guard); ReflectionHelpers.setField(MediaMuxer.class, realMuxer, "mLastTrackIndex", -1); // Pre-OREO jumps straight to nativeSetup inside the constructor. if (RuntimeEnvironment.getApiLevel() < O) { long nativeObject = nativeSetup(fd, format); ReflectionHelpers.setField(MediaMuxer.class, realMuxer, "mNativeObject", nativeObject); ReflectionHelpers.setField(MediaMuxer.class, realMuxer, "mState", MUXER_STATE_INITIALIZED); guard.open("release"); } else { ReflectionHelpers.callInstanceMethod( MediaMuxer.class, realMuxer, "setUpMediaMuxer", ReflectionHelpers.ClassParameter.from(FileDescriptor.class, fd), ReflectionHelpers.ClassParameter.from(int.class, format)); } } /** * Generates and returns an internal id to track the FileOutputStream corresponding to individual * MediaMuxer instances. */ @Implementation protected static long nativeSetup(@NonNull FileDescriptor fd, int format) throws IOException { FileOutputStream outputStream = fdToStream.get(fd); if (outputStream == null) { // If this MediaMuxer was constructed with the un-shadowed MediaMuxer(FileDescriptor, int), no // FileOutputStream will be associated with the FileDescriptor, so just create one. outputStream = new FileOutputStream(fd); fdToStream.put(fd, outputStream); } long potentialKey; do { potentialKey = random.nextLong(); } while (potentialKey == 0 || outputStreams.putIfAbsent(potentialKey, outputStream) != null); nextTrackIndices.put(potentialKey, new AtomicInteger(0)); return potentialKey; } /** Returns an incremented track id for the associated muxer. */ @Implementation protected static int nativeAddTrack( long nativeObject, @NonNull String[] keys, @NonNull Object[] values) { AtomicInteger nextTrackIndex = nextTrackIndices.get(nativeObject); if (nextTrackIndex == null) { throw new IllegalStateException("No next track index configured for key: " + nativeObject); } return nextTrackIndex.getAndIncrement(); } /** Writes the given data to the FileOutputStream for the associated muxer. */ @Implementation protected static void nativeWriteSampleData( long nativeObject, int trackIndex, @NonNull ByteBuffer byteBuf, int offset, int size, long presentationTimeUs, @MediaCodec.BufferFlag int flags) { byte[] bytes = new byte[size]; int oldPosition = byteBuf.position(); // The offset is the start-offset of the data in the buffer. We should use input offset for // byteBuf to read bytes, instead of byteBuf current offset. // See https://developer.android.com/reference/android/media/MediaCodec.BufferInfo#offset. byteBuf.position(offset); byteBuf.get(bytes, 0, size); byteBuf.position(oldPosition); try { getStream(nativeObject).write(bytes); } catch (IOException e) { throw new RuntimeException("Unable to write to temporary file.", e); } } /** Closes the FileOutputStream for the associated muxer. */ @Implementation protected static void nativeStop(long nativeObject) { try { // Close the output stream. getStream(nativeObject).close(); // Clear the stream from both internal caches. fdToStream.remove(outputStreams.remove(nativeObject).getFD()); } catch (IOException e) { throw new RuntimeException("Unable to close temporary file.", e); } } private static FileOutputStream getStream(long streamKey) { FileOutputStream stream = outputStreams.get(streamKey); if (stream == null) { throw new IllegalStateException("No output stream configured for key: " + streamKey); } return stream; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy