Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F32509581
D14478.1767109185.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
7 KB
Referenced Files
None
Subscribers
None
D14478.1767109185.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D14478: [android] Implement transcodeVideo() function on Android
Attached
Detach File
Event Timeline
Log In to Comment