Page MenuHomePhorge

D14478.1767109185.diff
No OneTemporary

Size
7 KB
Referenced Files
None
Subscribers
None

D14478.1767109185.diff

diff --git a/native/expo-modules/comm-expo-package/android/build.gradle b/native/expo-modules/comm-expo-package/android/build.gradle
--- a/native/expo-modules/comm-expo-package/android/build.gradle
+++ b/native/expo-modules/comm-expo-package/android/build.gradle
@@ -93,4 +93,8 @@
// dependencies of AndroidLifecycleModule
implementation "androidx.lifecycle:lifecycle-runtime:2.5.1"
implementation "androidx.lifecycle:lifecycle-process:2.5.1"
+
+ // dependencies of MediaModule
+ // keep in sync with Expo's media3 dependencies
+ implementation 'androidx.media3:media3-transformer:1.4.0'
}
diff --git a/native/expo-modules/comm-expo-package/android/src/main/java/app/comm/android/media/MediaModule.kt b/native/expo-modules/comm-expo-package/android/src/main/java/app/comm/android/media/MediaModule.kt
--- a/native/expo-modules/comm-expo-package/android/src/main/java/app/comm/android/media/MediaModule.kt
+++ b/native/expo-modules/comm-expo-package/android/src/main/java/app/comm/android/media/MediaModule.kt
@@ -5,16 +5,37 @@
import android.graphics.ImageDecoder
import android.graphics.Movie
import android.graphics.drawable.AnimatedImageDrawable
+import android.media.MediaCodecInfo.CodecProfileLevel
import android.media.MediaExtractor
import android.media.MediaFormat
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import androidx.annotation.OptIn
+import androidx.media3.common.Format
+import androidx.media3.common.MediaItem
+import androidx.media3.common.MimeTypes
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.effect.Presentation
+import androidx.media3.transformer.Composition
+import androidx.media3.transformer.DefaultEncoderFactory
+import androidx.media3.transformer.EditedMediaItem
+import androidx.media3.transformer.Effects
+import androidx.media3.transformer.ExportException
+import androidx.media3.transformer.ExportResult
+import androidx.media3.transformer.ProgressHolder
+import androidx.media3.transformer.Transformer
+import androidx.media3.transformer.VideoEncoderSettings
+import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.CodedException
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
+import expo.modules.kotlin.types.Enumerable
+import java.io.File
import java.io.FileOutputStream
class VideoInfo : Record {
@@ -34,6 +55,39 @@
var format: String = "N/A"
}
+enum class H264Profile(val value: String) : Enumerable {
+ baseline("baseline"),
+ main("main"),
+ high("high"),
+}
+
+
+class TranscodeOptions : Record {
+ @Field
+ var width: Double = -1.0
+
+ @Field
+ var height: Double = -1.0
+
+ @Field
+ var bitrate: Int = -1
+
+ @Field
+ var profile: H264Profile = H264Profile.high
+}
+
+class TranscodeStats : Record {
+ @Field
+ var size: Long = 0
+
+ @Field
+ var duration: Int = 0
+
+ @Field
+ var speed: Double = 0.0
+}
+
+const val TRANSCODE_PROGRESS_EVENT_NAME = "onTranscodeProgress"
class MediaModule : Module() {
override fun definition() = ModuleDefinition {
@@ -42,6 +96,9 @@
AsyncFunction("getVideoInfo", this@MediaModule::getVideoInfo)
AsyncFunction("hasMultipleFrames", this@MediaModule::hasMultipleFrames)
AsyncFunction("generateThumbnail", this@MediaModule::generateThumbnail)
+ AsyncFunction("transcodeVideo", this@MediaModule::transcodeVideo)
+
+ Events(TRANSCODE_PROGRESS_EVENT_NAME)
}
@@ -83,7 +140,6 @@
return videoInfo
}
-
private fun hasMultipleFrames(path: String): Boolean {
val uri = Uri.parse(path)
try {
@@ -129,6 +185,118 @@
}
}
+ // media3 library has unstable api
+ @OptIn(UnstableApi::class)
+ private fun transcodeVideo(
+ inputPath: String,
+ outputPath: String,
+ options: TranscodeOptions,
+ promise: Promise
+ ) {
+ val inputUri = Uri.parse(inputPath)
+ if(inputUri == null) {
+ promise.reject(TranscodingFailed(Exception("Invalid input url: $inputPath")))
+ return
+ }
+ val outputUri = Uri.parse(outputPath).path
+ if(outputUri == null) {
+ promise.reject(TranscodingFailed(Exception("Invalid output url: $outputPath")))
+ return
+ }
+
+ val mediaItem = MediaItem.Builder()
+ .setUri(inputUri)
+ .build()
+
+ val effects = Effects(
+ listOf(), listOf(
+ Presentation.createForWidthAndHeight(
+ options.width.toInt(),
+ options.height.toInt(),
+ Presentation.LAYOUT_SCALE_TO_FIT
+ )
+ )
+ )
+
+ val editedMediaItem = EditedMediaItem.Builder(mediaItem)
+ .setEffects(effects)
+ .build()
+
+ val profile = when (options.profile) {
+ H264Profile.baseline -> CodecProfileLevel.AVCProfileBaseline
+ H264Profile.main -> CodecProfileLevel.AVCProfileMain
+ H264Profile.high -> CodecProfileLevel.AVCProfileHigh
+ }
+
+ val videoEncoderFactory = DefaultEncoderFactory.Builder(context)
+ .setRequestedVideoEncoderSettings(
+ VideoEncoderSettings.DEFAULT.buildUpon()
+ .setBitrate(if (options.bitrate == -1) Format.NO_VALUE else options.bitrate * 1000)
+ .setEncodingProfileLevel(profile, Format.NO_VALUE)
+ .build()
+ )
+ .build()
+
+
+ val newFile = File(outputUri)
+
+ if (!newFile.exists()) {
+ val created = newFile.createNewFile()
+ if (!created) {
+ promise.reject(FailedToCreateFile(outputPath))
+ return
+ }
+ }
+ var transformer: Transformer? = null
+ val handler = Handler(Looper.getMainLooper())
+ val progressHolder = ProgressHolder()
+ val runnable = object : Runnable {
+ override fun run() {
+ transformer?.getProgress(progressHolder)
+ sendEvent(
+ TRANSCODE_PROGRESS_EVENT_NAME,
+ mapOf("progress" to progressHolder.progress / 100.0)
+ )
+ handler.postDelayed(this, 200)
+ }
+ }
+
+ handler.post {
+ val startTime = System.currentTimeMillis()
+ transformer = Transformer.Builder(context)
+ .setVideoMimeType(MimeTypes.VIDEO_H264)
+ .setAudioMimeType(MimeTypes.AUDIO_AAC)
+ .setEncoderFactory(videoEncoderFactory)
+ .addListener(object : Transformer.Listener {
+ override fun onCompleted(composition: Composition, exportResult: ExportResult) {
+ handler.removeCallbacks(runnable)
+ sendEvent(
+ TRANSCODE_PROGRESS_EVENT_NAME,
+ mapOf("progress" to 1)
+ )
+ val stats = TranscodeStats()
+ stats.duration = (exportResult.durationMs / 1000).toInt()
+ stats.size = exportResult.fileSizeBytes
+ val endTime = System.currentTimeMillis()
+ stats.speed = (exportResult.durationMs).toDouble() / (endTime - startTime)
+ promise.resolve(stats)
+ }
+
+ override fun onError(
+ composition: Composition,
+ exportResult: ExportResult,
+ exportException: ExportException
+ ) {
+ handler.removeCallbacks(runnable)
+ promise.reject(TranscodingFailed(exportException))
+ }
+ })
+ .build()
+ transformer?.start(editedMediaItem, outputUri)
+ }
+ handler.post(runnable)
+ }
+
private val context: Context
get() = requireNotNull(this.appContext.reactContext) {
"React Application Context is null"
@@ -154,4 +322,9 @@
private class SaveThumbnailException(uri: String, cause: Throwable) :
CodedException("Could not save thumbnail to $uri", cause)
+private class FailedToCreateFile(uri: String) :
+ CodedException("Failed to create file $uri")
+
+private class TranscodingFailed(cause: Throwable) :
+ CodedException("Transcoding failed: ", cause)
// endregion

File Metadata

Mime Type
text/plain
Expires
Tue, Dec 30, 3:39 PM (3 h, 24 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5867434
Default Alt Text
D14478.1767109185.diff (7 KB)

Event Timeline