Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package com.sprout.sprout_mobile
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.media.MediaExtractor
import android.media.MediaMuxer
import android.os.Build
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.ByteBuffer
import java.util.UUID

class MainActivity : FlutterActivity() {
private var mediaUploadChannel: MethodChannel? = null
Expand All @@ -28,6 +32,9 @@ class MainActivity : FlutterActivity() {
TRANSCODE_IMAGE_TO_JPEG_METHOD -> {
handleTranscodeImageToJpeg(call.arguments, result)
}
TRANSCODE_VIDEO_TO_MP4_METHOD -> {
handleTranscodeVideoToMp4(call.arguments, result)
}
else -> result.notImplemented()
}
}
Expand Down Expand Up @@ -150,6 +157,63 @@ class MainActivity : FlutterActivity() {
result.success(transformedBytes)
}

private fun handleTranscodeVideoToMp4(
arguments: Any?,
result: MethodChannel.Result,
) {
val sourcePath = arguments as? String ?: run {
invalidArguments(result, "Expected source file path as String.")
return
}

Thread {
val outputFile = File(cacheDir, "${UUID.randomUUID()}.mp4")
var muxer: MediaMuxer? = null
val extractor = MediaExtractor()
try {
extractor.setDataSource(sourcePath)
muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)

val trackIndices = mutableMapOf<Int, Int>()
for (i in 0 until extractor.trackCount) {
val format = extractor.getTrackFormat(i)
val newIndex = muxer.addTrack(format)
trackIndices[i] = newIndex
extractor.selectTrack(i)
}

muxer.start()
val buffer = ByteBuffer.allocate(1024 * 1024) // 1MB buffer
val bufferInfo = android.media.MediaCodec.BufferInfo()

while (true) {
val sampleSize = extractor.readSampleData(buffer, 0)
if (sampleSize < 0) break
val muxerTrack = trackIndices[extractor.sampleTrackIndex]!!
bufferInfo.offset = 0
bufferInfo.size = sampleSize
bufferInfo.presentationTimeUs = extractor.sampleTime
bufferInfo.flags = extractor.sampleFlags
muxer.writeSampleData(muxerTrack, buffer, bufferInfo)
extractor.advance()
}

muxer.stop()
result.success(outputFile.absolutePath)
} catch (e: Exception) {
outputFile.delete()
result.error(
"transcode_failed",
e.message ?: "Video transcoding failed.",
null,
)
} finally {
try { muxer?.release() } catch (_: Exception) {}
extractor.release()
}
}.start()
}

private fun invalidArguments(
result: MethodChannel.Result,
message: String,
Expand All @@ -161,5 +225,6 @@ class MainActivity : FlutterActivity() {
private const val MEDIA_UPLOAD_CHANNEL = "sprout/media_upload"
private const val SANITIZE_IMAGE_FOR_UPLOAD_METHOD = "sanitizeImageForUpload"
private const val TRANSCODE_IMAGE_TO_JPEG_METHOD = "transcodeImageToJpeg"
private const val TRANSCODE_VIDEO_TO_MP4_METHOD = "transcodeVideoToMp4"
}
}
62 changes: 62 additions & 0 deletions mobile/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AVFoundation
import Flutter
import UIKit

Expand Down Expand Up @@ -103,8 +104,69 @@ import UIKit
}

result(FlutterStandardTypedData(bytes: jpegData))
case "transcodeVideoToMp4":
guard let sourcePath = call.arguments as? String else {
result(
FlutterError(
code: "invalid_arguments",
message: "Expected source file path as String.",
details: nil
)
)
return
}
transcodeVideoToMp4(sourcePath: sourcePath, result: result)
default:
result(FlutterMethodNotImplemented)
}
}

private func transcodeVideoToMp4(
sourcePath: String,
result: @escaping FlutterResult
) {
let sourceURL = URL(fileURLWithPath: sourcePath)
let asset = AVURLAsset(url: sourceURL)

guard let exportSession = AVAssetExportSession(
asset: asset,
presetName: AVAssetExportPresetPassthrough
) else {
result(
FlutterError(
code: "transcode_failed",
message: "Unable to create export session.",
details: nil
)
)
return
}

let outputURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")

exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.shouldOptimizeForNetworkUse = true

exportSession.exportAsynchronously {
switch exportSession.status {
case .completed:
result(outputURL.path)
default:
let errorMessage = exportSession.error?.localizedDescription
?? "Video transcoding failed with status \(exportSession.status.rawValue)."
result(
FlutterError(
code: "transcode_failed",
message: errorMessage,
details: nil
)
)
// Clean up partial output on failure.
try? FileManager.default.removeItem(at: outputURL)
}
}
}
}
Loading