Improve: Video compression with progress updates
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-03-13 01:35:04 +01:00
parent 31caa133dc
commit c85914672e
22 changed files with 9494 additions and 136 deletions

View file

@ -3,6 +3,7 @@
## 0.0.98 ## 0.0.98
- Fix: Issue with contact requests - Fix: Issue with contact requests
- Improve: Video compression with progress updates
## 0.0.96 ## 0.0.96

View file

@ -72,4 +72,5 @@ flutter {
dependencies { dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
implementation 'com.otaliastudios:transcoder:0.11.0'
} }

View file

@ -1,27 +1,14 @@
package eu.twonly package eu.twonly
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.android.FlutterFragmentActivity
import android.view.KeyEvent import android.view.KeyEvent
import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownPlugin.eventSink import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownPlugin.eventSink
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
import android.view.KeyEvent.KEYCODE_VOLUME_UP import android.view.KeyEvent.KEYCODE_VOLUME_UP
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.OutputStream
class MainActivity : FlutterFragmentActivity() { class MainActivity : FlutterFragmentActivity() {
private val MEDIA_STORE_CHANNEL = "eu.twonly/mediaStore"
private lateinit var channel: MethodChannel
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) { if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) {
eventSink!!.success(true) eventSink!!.success(true)
@ -37,80 +24,7 @@ class MainActivity : FlutterFragmentActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, MEDIA_STORE_CHANNEL) MediaStoreChannel.configure(flutterEngine, applicationContext)
VideoCompressionChannel.configure(flutterEngine, applicationContext)
channel.setMethodCallHandler {call, result ->
try {
if (call.method == "safeFileToDownload") {
val arguments = call.arguments<Map<String, String>>() as Map<String, String>
val sourceFile = arguments["sourceFile"]
if (sourceFile == null) {
result.success(false)
} else {
val context = applicationContext
val inputStream = FileInputStream(File(sourceFile))
val outputName = File(sourceFile).name.takeIf { it.isNotEmpty() } ?: "memories.zip"
val savedUri = saveZipToDownloads(context, outputName, inputStream)
if (savedUri != null) {
result.success(savedUri.toString())
} else {
result.error("SAVE_FAILED", "Could not save ZIP", null)
}
}
} else {
result.notImplemented()
}
} catch (e: Exception) {
result.error("EXCEPTION", e.message, null)
}
}
}
}
fun saveZipToDownloads(
context: Context,
fileName: String = "archive.zip",
sourceStream: InputStream
): android.net.Uri? {
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "application/zip")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
put(MediaStore.MediaColumns.IS_PENDING, 1)
}
}
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Files.getContentUri("external")
}
val uri = resolver.insert(collection, contentValues) ?: return null
try {
resolver.openOutputStream(uri).use { out: OutputStream? ->
requireNotNull(out) { "Unable to open output stream" }
sourceStream.use { input ->
input.copyTo(out)
}
out.flush()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val done = ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) }
resolver.update(uri, done, null, null)
}
return uri
} catch (e: Exception) {
try { resolver.delete(uri, null, null) } catch (_: Exception) {}
return null
} }
} }

View file

@ -0,0 +1,92 @@
package eu.twonly
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.OutputStream
object MediaStoreChannel {
private const val CHANNEL = "eu.twonly/mediaStore"
fun configure(flutterEngine: FlutterEngine, context: Context) {
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
channel.setMethodCallHandler { call, result ->
try {
if (call.method == "safeFileToDownload") {
val arguments = call.arguments<Map<String, String>>() as Map<String, String>
val sourceFile = arguments["sourceFile"]
if (sourceFile == null) {
result.success(false)
} else {
val inputStream = FileInputStream(File(sourceFile))
val outputName = File(sourceFile).name.takeIf { it.isNotEmpty() } ?: "memories.zip"
val savedUri = saveZipToDownloads(context, outputName, inputStream)
if (savedUri != null) {
result.success(savedUri.toString())
} else {
result.error("SAVE_FAILED", "Could not save ZIP", null)
}
}
} else {
result.notImplemented()
}
} catch (e: Exception) {
result.error("EXCEPTION", e.message, null)
}
}
}
private fun saveZipToDownloads(
context: Context,
fileName: String = "archive.zip",
sourceStream: InputStream
): android.net.Uri? {
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "application/zip")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
put(MediaStore.MediaColumns.IS_PENDING, 1)
}
}
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Files.getContentUri("external")
}
val uri = resolver.insert(collection, contentValues) ?: return null
try {
resolver.openOutputStream(uri).use { out: OutputStream? ->
requireNotNull(out) { "Unable to open output stream" }
sourceStream.use { input ->
input.copyTo(out)
}
out.flush()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val done = ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) }
resolver.update(uri, done, null, null)
}
return uri
} catch (e: Exception) {
try { resolver.delete(uri, null, null) } catch (_: Exception) {}
return null
}
}
}

View file

@ -1,6 +0,0 @@
package eu.twonly
class MyMediaStorageProxy {
}

View file

@ -0,0 +1,107 @@
package eu.twonly
import android.content.Context
import android.media.MediaFormat
import android.os.Handler
import android.os.Looper
import com.otaliastudios.transcoder.Transcoder
import com.otaliastudios.transcoder.TranscoderListener
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
import com.otaliastudios.transcoder.strategy.TrackStrategy
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
object VideoCompressionChannel {
private const val CHANNEL = "eu.twonly/videoCompression"
// Compression parameters defined natively (as requested)
private const val VIDEO_BITRATE = 2_000_000L // 2 Mbps
// Audio parameters defined natively
private const val AUDIO_BITRATE = 128_000L // 128 kbps
private const val AUDIO_SAMPLE_RATE = 44_100
private const val AUDIO_CHANNELS = 2
fun configure(flutterEngine: FlutterEngine, context: Context) {
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
channel.setMethodCallHandler { call, result ->
try {
if (call.method == "compressVideo") {
val arguments = call.arguments<Map<String, Any>>() ?: emptyMap()
val inputPath = arguments["input"] as? String
val outputPath = arguments["output"] as? String
if (inputPath == null || outputPath == null) {
result.error("INVALID_ARGS", "Input or output path missing", null)
return@setMethodCallHandler
}
val mainHandler = Handler(Looper.getMainLooper())
val baseVideoStrategy = DefaultVideoStrategy.Builder()
.keyFrameInterval(3f)
.bitRate(VIDEO_BITRATE)
.addResizer(com.otaliastudios.transcoder.resize.AtMostResizer(1920, 1080))
.build()
val trackStrategyClass = TrackStrategy::class.java
val hevcStrategy = java.lang.reflect.Proxy.newProxyInstance(
trackStrategyClass.classLoader,
arrayOf(trackStrategyClass)
) { _, method, args ->
val result = if (args != null) method.invoke(baseVideoStrategy, *args) else method.invoke(baseVideoStrategy)
if (method.name == "createOutputFormat" && result is MediaFormat) {
result.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC)
}
result
} as TrackStrategy
Transcoder.into(outputPath)
.addDataSource(inputPath)
.setVideoTrackStrategy(hevcStrategy)
.setAudioTrackStrategy(
DefaultAudioStrategy.builder()
.channels(AUDIO_CHANNELS)
.sampleRate(AUDIO_SAMPLE_RATE)
.bitRate(AUDIO_BITRATE)
.build()
)
.setListener(object : TranscoderListener {
override fun onTranscodeProgress(progress: Double) {
mainHandler.post {
val mappedProgress = (progress * 100).toInt()
channel.invokeMethod("onProgress", mapOf("progress" to mappedProgress))
}
}
override fun onTranscodeCompleted(successCode: Int) {
mainHandler.post {
result.success(outputPath)
}
}
override fun onTranscodeCanceled() {
mainHandler.post {
result.error("CANCELED", "Video compression canceled", null)
}
}
override fun onTranscodeFailed(exception: Throwable) {
mainHandler.post {
result.error("FAILED", exception.message, null)
}
}
})
.transcode()
} else {
result.notImplemented()
}
} catch (e: Exception) {
result.error("EXCEPTION", e.message, null)
}
}
}
}

View file

@ -316,8 +316,6 @@ PODS:
- SwiftyGif (5.4.5) - SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- video_compress (0.3.0):
- Flutter
- video_player_avfoundation (0.0.1): - video_player_avfoundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@ -366,7 +364,6 @@ DEPENDENCIES:
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- SwiftProtobuf - SwiftProtobuf
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_compress (from `.symlinks/plugins/video_compress/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`) - workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
@ -473,8 +470,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
video_compress:
:path: ".symlinks/plugins/video_compress/ios"
video_player_avfoundation: video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin" :path: ".symlinks/plugins/video_player_avfoundation/darwin"
workmanager_apple: workmanager_apple:
@ -545,7 +540,6 @@ SPEC CHECKSUMS:
SwiftProtobuf: c901f00a3e125dc33cac9b16824da85682ee47da SwiftProtobuf: c901f00a3e125dc33cac9b16824da85682ee47da
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_compress: f2133a07762889d67f0711ac831faa26f956980e
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778

View file

@ -21,6 +21,7 @@
D21FCEAB2D9F2B750088701D /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D21FCEA42D9F2B750088701D /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D21FCEAB2D9F2B750088701D /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D21FCEA42D9F2B750088701D /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25D4D1D2EF626E30029F805 /* StoreKit.framework */; }; D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25D4D1D2EF626E30029F805 /* StoreKit.framework */; };
D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */; };
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; }; F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -107,6 +108,7 @@
D25D4D1D2EF626E30029F805 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; D25D4D1D2EF626E30029F805 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
D25D4D702EFF41DB0029F805 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D25D4D702EFF41DB0029F805 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; }; D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; };
D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCompressionChannel.swift; sourceTree = "<group>"; };
DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E190E82D9973B318A389650B /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E190E82D9973B318A389650B /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
@ -235,6 +237,7 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */,
D24E27CC2F38ABC10055D9D1 /* RunnerRelease.entitlements */, D24E27CC2F38ABC10055D9D1 /* RunnerRelease.entitlements */,
D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */, D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */,
D2265DD42D920142000D99BB /* Runner.entitlements */, D2265DD42D920142000D99BB /* Runner.entitlements */,
@ -624,6 +627,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
); );

View file

@ -12,6 +12,11 @@ import flutter_sharing_intent
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
if let registrar = self.registrar(forPlugin: "VideoCompressionChannel") {
VideoCompressionChannel.register(with: registrar.messenger())
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }

View file

@ -0,0 +1,253 @@
import Foundation
import Flutter
import AVFoundation
class VideoCompressionChannel {
private let channelName = "eu.twonly/videoCompression"
// Hold a strong reference so the instance isn't immediately deallocated
private static var activeInstance: VideoCompressionChannel?
static func register(with messenger: FlutterBinaryMessenger) {
let instance = VideoCompressionChannel()
activeInstance = instance
let channel = FlutterMethodChannel(name: instance.channelName, binaryMessenger: messenger)
print("[VideoCompressionChannel] Registered channel: \(instance.channelName)")
channel.setMethodCallHandler { [weak instance] (call: FlutterMethodCall, result: @escaping FlutterResult) in
print("[VideoCompressionChannel] Received method call: \(call.method)")
instance?.handle(call, result: result, channel: channel)
}
}
init() {}
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult, channel: FlutterMethodChannel) {
if call.method == "compressVideo" {
guard let args = call.arguments as? [String: Any],
let inputPath = args["input"] as? String,
let outputPath = args["output"] as? String else {
print("[VideoCompressionChannel] Error: Missing input or output path in arguments")
result(FlutterError(code: "INVALID_ARGS", message: "Input or output path missing", details: nil))
return
}
print("[VideoCompressionChannel] Starting compressVideo from \(inputPath) to \(outputPath)")
compress(inputPath: inputPath, outputPath: outputPath, channel: channel, result: result)
} else {
print("[VideoCompressionChannel] Method not implemented: \(call.method)")
result(FlutterMethodNotImplemented)
}
}
func compress(inputPath: String, outputPath: String, channel: FlutterMethodChannel, result: @escaping FlutterResult) {
let inputURL = URL(fileURLWithPath: inputPath)
let outputURL = URL(fileURLWithPath: outputPath)
if FileManager.default.fileExists(atPath: outputURL.path) {
print("[VideoCompressionChannel] Removing existing file at output path")
try? FileManager.default.removeItem(at: outputURL)
}
let asset = AVAsset(url: inputURL)
guard let videoTrack = asset.tracks(withMediaType: .video).first else {
print("[VideoCompressionChannel] Error: No video track found in asset")
result(FlutterError(code: "NO_VIDEO_TRACK", message: "Video track not found", details: nil))
return
}
let naturalSize = videoTrack.naturalSize
let transform = videoTrack.preferredTransform
let isPortrait = transform.a == 0 && abs(transform.b) == 1.0 && abs(transform.c) == 1.0 && transform.d == 0
let originalWidth = isPortrait ? naturalSize.height : naturalSize.width
let originalHeight = isPortrait ? naturalSize.width : naturalSize.height
let maxDimension: CGFloat = 1920.0
let minDimension: CGFloat = 1080.0
var targetWidth = originalWidth
var targetHeight = originalHeight
if targetWidth > maxDimension || targetHeight > maxDimension {
let widthRatio = maxDimension / targetWidth
let heightRatio = minDimension / targetHeight
let scaleFactor = min(widthRatio, heightRatio)
targetWidth *= scaleFactor
targetHeight *= scaleFactor
}
let targetBitrate = 3_000_000
do {
let reader = try AVAssetReader(asset: asset)
let writer = try AVAssetWriter(outputURL: outputURL, fileType: .mp4)
writer.shouldOptimizeForNetworkUse = true
let videoSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.hevc,
AVVideoWidthKey: Int(targetWidth),
AVVideoHeightKey: Int(targetHeight),
AVVideoCompressionPropertiesKey: [
AVVideoAverageBitRateKey: targetBitrate
]
]
let readerOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
])
let writerInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
writerInput.expectsMediaDataInRealTime = false
writerInput.transform = videoTrack.preferredTransform
guard writer.canAdd(writerInput) else {
result(FlutterError(code: "WRITER_ERROR", message: "Cannot add video writer input", details: nil))
return
}
guard reader.canAdd(readerOutput) else {
result(FlutterError(code: "READER_ERROR", message: "Cannot add video reader output", details: nil))
return
}
reader.add(readerOutput)
writer.add(writerInput)
// Audio processing (re-encode to AAC)
var audioReaderOutput: AVAssetReaderTrackOutput?
var audioWriterInput: AVAssetWriterInput?
if let audioTrack = asset.tracks(withMediaType: .audio).first {
let audioReaderSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatLinearPCM
]
let aReaderOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: audioReaderSettings)
let audioWriterSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey: 2,
AVSampleRateKey: 44100,
AVEncoderBitRateKey: 128000
]
let aWriterInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioWriterSettings)
aWriterInput.expectsMediaDataInRealTime = false
if reader.canAdd(aReaderOutput) && writer.canAdd(aWriterInput) {
reader.add(aReaderOutput)
writer.add(aWriterInput)
audioReaderOutput = aReaderOutput
audioWriterInput = aWriterInput
} else {
print("[VideoCompressionChannel] Warning: Cannot add audio tracks, proceeding without audio")
}
}
guard reader.startReading() else {
result(FlutterError(code: "READER_ERROR", message: "Cannot start reading: \(reader.error?.localizedDescription ?? "unknown error")", details: nil))
return
}
guard writer.startWriting() else {
result(FlutterError(code: "WRITER_ERROR", message: "Cannot start writing: \(writer.error?.localizedDescription ?? "unknown error")", details: nil))
return
}
writer.startSession(atSourceTime: .zero)
let duration = CMTimeGetSeconds(asset.duration)
let videoQueue = DispatchQueue(label: "videoQueue")
let audioQueue = DispatchQueue(label: "audioQueue")
let group = DispatchGroup()
// State tracking flag to avoid sending completed messages prematurely
var isVideoCompleted = false
var isAudioCompleted = audioWriterInput == nil
group.enter()
writerInput.requestMediaDataWhenReady(on: videoQueue) {
while writerInput.isReadyForMoreMediaData {
if reader.status != .reading {
if !isVideoCompleted {
isVideoCompleted = true
writerInput.markAsFinished()
group.leave()
}
return
}
if let sampleBuffer = readerOutput.copyNextSampleBuffer() {
let presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let timeInSeconds = CMTimeGetSeconds(presentationTime)
if duration > 0 {
let progress = Int((timeInSeconds / duration) * 100)
DispatchQueue.main.async {
channel.invokeMethod("onProgress", arguments: ["progress": progress])
}
}
writerInput.append(sampleBuffer)
} else {
if !isVideoCompleted {
isVideoCompleted = true
writerInput.markAsFinished()
group.leave()
}
break
}
}
}
if let audioWriterInput = audioWriterInput, let audioReaderOutput = audioReaderOutput {
group.enter()
audioWriterInput.requestMediaDataWhenReady(on: audioQueue) {
while audioWriterInput.isReadyForMoreMediaData {
if reader.status != .reading {
if !isAudioCompleted {
isAudioCompleted = true
audioWriterInput.markAsFinished()
group.leave()
}
return
}
if let sampleBuffer = audioReaderOutput.copyNextSampleBuffer() {
audioWriterInput.append(sampleBuffer)
} else {
if !isAudioCompleted {
isAudioCompleted = true
audioWriterInput.markAsFinished()
group.leave()
}
break
}
}
}
}
group.notify(queue: .main) {
if reader.status == .completed {
writer.finishWriting {
if writer.status == .completed {
print("[VideoCompressionChannel] Compression completed successfully!")
result(outputPath)
} else {
print("[VideoCompressionChannel] Writer Error: \(writer.error?.localizedDescription ?? "Unknown error")")
result(FlutterError(code: "WRITER_ERROR", message: writer.error?.localizedDescription, details: nil))
}
}
} else {
writer.cancelWriting()
print("[VideoCompressionChannel] Reader Error: \(reader.error?.localizedDescription ?? "Unknown error")")
result(FlutterError(code: "READER_ERROR", message: reader.error?.localizedDescription, details: nil))
}
}
} catch {
print("[VideoCompressionChannel] Exception: \(error.localizedDescription)")
result(FlutterError(code: "COMPRESS_ERROR", message: error.localizedDescription, details: nil))
}
}
}

View file

@ -0,0 +1,45 @@
import 'package:flutter/services.dart';
import 'package:twonly/src/utils/log.dart';
abstract class VideoCompressionChannel {
static const MethodChannel _channel =
MethodChannel('eu.twonly/videoCompression');
static void Function(double)? _currentProgressCallback;
static bool _handlerSetup = false;
static void _setupProgressHandler() {
if (_handlerSetup) return;
_channel.setMethodCallHandler((call) async {
if (call.method == 'onProgress') {
// ignore: avoid_dynamic_calls
final progress = call.arguments['progress'] as int;
_currentProgressCallback?.call(progress / 100.0);
}
});
_handlerSetup = true;
}
static Future<String?> compressVideo({
required String inputPath,
required String outputPath,
void Function(double progress)? onProgress,
}) async {
try {
_setupProgressHandler();
_currentProgressCallback = onProgress;
await _channel.invokeMethod('compressVideo', {
'input': inputPath,
'output': outputPath,
});
return outputPath;
} on PlatformException catch (e) {
Log.error('Failed to compress video: $e');
return null;
} finally {
_currentProgressCallback = null;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -48,6 +48,8 @@ class MediaFiles extends Table {
BoolColumn get stored => boolean().withDefault(const Constant(false))(); BoolColumn get stored => boolean().withDefault(const Constant(false))();
BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))(); BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))();
IntColumn get preProgressingProcess => integer().nullable()();
TextColumn get reuploadRequestedBy => TextColumn get reuploadRequestedBy =>
text().map(IntListTypeConverter()).nullable()(); text().map(IntListTypeConverter()).nullable()();

View file

@ -62,7 +62,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 8; int get schemaVersion => 9;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
@ -131,6 +131,12 @@ class TwonlyDB extends _$TwonlyDB {
// ignore: experimental_member_use // ignore: experimental_member_use
await m.alterTable(TableMigration(schema.messageActions)); await m.alterTable(TableMigration(schema.messageActions));
}, },
from8To9: (m, schema) async {
await m.addColumn(
schema.mediaFiles,
schema.mediaFiles.preProgressingProcess,
);
},
)(m, from, to); )(m, from, to);
}, },
); );

View file

@ -1950,6 +1950,12 @@ class $MediaFilesTable extends MediaFiles
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("is_draft_media" IN (0, 1))'), 'CHECK ("is_draft_media" IN (0, 1))'),
defaultValue: const Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _preProgressingProcessMeta =
const VerificationMeta('preProgressingProcess');
@override
late final GeneratedColumn<int> preProgressingProcess = GeneratedColumn<int>(
'pre_progressing_process', aliasedName, true,
type: DriftSqlType.int, requiredDuringInsert: false);
@override @override
late final GeneratedColumnWithTypeConverter<List<int>?, String> late final GeneratedColumnWithTypeConverter<List<int>?, String>
reuploadRequestedBy = GeneratedColumn<String>( reuploadRequestedBy = GeneratedColumn<String>(
@ -2019,6 +2025,7 @@ class $MediaFilesTable extends MediaFiles
requiresAuthentication, requiresAuthentication,
stored, stored,
isDraftMedia, isDraftMedia,
preProgressingProcess,
reuploadRequestedBy, reuploadRequestedBy,
displayLimitInMilliseconds, displayLimitInMilliseconds,
removeAudio, removeAudio,
@ -2061,6 +2068,12 @@ class $MediaFilesTable extends MediaFiles
isDraftMedia.isAcceptableOrUnknown( isDraftMedia.isAcceptableOrUnknown(
data['is_draft_media']!, _isDraftMediaMeta)); data['is_draft_media']!, _isDraftMediaMeta));
} }
if (data.containsKey('pre_progressing_process')) {
context.handle(
_preProgressingProcessMeta,
preProgressingProcess.isAcceptableOrUnknown(
data['pre_progressing_process']!, _preProgressingProcessMeta));
}
if (data.containsKey('display_limit_in_milliseconds')) { if (data.containsKey('display_limit_in_milliseconds')) {
context.handle( context.handle(
_displayLimitInMillisecondsMeta, _displayLimitInMillisecondsMeta,
@ -2134,6 +2147,8 @@ class $MediaFilesTable extends MediaFiles
.read(DriftSqlType.bool, data['${effectivePrefix}stored'])!, .read(DriftSqlType.bool, data['${effectivePrefix}stored'])!,
isDraftMedia: attachedDatabase.typeMapping isDraftMedia: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}is_draft_media'])!, .read(DriftSqlType.bool, data['${effectivePrefix}is_draft_media'])!,
preProgressingProcess: attachedDatabase.typeMapping.read(
DriftSqlType.int, data['${effectivePrefix}pre_progressing_process']),
reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn
.fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string,
data['${effectivePrefix}reupload_requested_by'])), data['${effectivePrefix}reupload_requested_by'])),
@ -2189,6 +2204,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
final bool requiresAuthentication; final bool requiresAuthentication;
final bool stored; final bool stored;
final bool isDraftMedia; final bool isDraftMedia;
final int? preProgressingProcess;
final List<int>? reuploadRequestedBy; final List<int>? reuploadRequestedBy;
final int? displayLimitInMilliseconds; final int? displayLimitInMilliseconds;
final bool? removeAudio; final bool? removeAudio;
@ -2206,6 +2222,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
required this.requiresAuthentication, required this.requiresAuthentication,
required this.stored, required this.stored,
required this.isDraftMedia, required this.isDraftMedia,
this.preProgressingProcess,
this.reuploadRequestedBy, this.reuploadRequestedBy,
this.displayLimitInMilliseconds, this.displayLimitInMilliseconds,
this.removeAudio, this.removeAudio,
@ -2234,6 +2251,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
map['requires_authentication'] = Variable<bool>(requiresAuthentication); map['requires_authentication'] = Variable<bool>(requiresAuthentication);
map['stored'] = Variable<bool>(stored); map['stored'] = Variable<bool>(stored);
map['is_draft_media'] = Variable<bool>(isDraftMedia); map['is_draft_media'] = Variable<bool>(isDraftMedia);
if (!nullToAbsent || preProgressingProcess != null) {
map['pre_progressing_process'] = Variable<int>(preProgressingProcess);
}
if (!nullToAbsent || reuploadRequestedBy != null) { if (!nullToAbsent || reuploadRequestedBy != null) {
map['reupload_requested_by'] = Variable<String>($MediaFilesTable map['reupload_requested_by'] = Variable<String>($MediaFilesTable
.$converterreuploadRequestedByn .$converterreuploadRequestedByn
@ -2278,6 +2298,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
requiresAuthentication: Value(requiresAuthentication), requiresAuthentication: Value(requiresAuthentication),
stored: Value(stored), stored: Value(stored),
isDraftMedia: Value(isDraftMedia), isDraftMedia: Value(isDraftMedia),
preProgressingProcess: preProgressingProcess == null && nullToAbsent
? const Value.absent()
: Value(preProgressingProcess),
reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(reuploadRequestedBy), : Value(reuploadRequestedBy),
@ -2322,6 +2345,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
serializer.fromJson<bool>(json['requiresAuthentication']), serializer.fromJson<bool>(json['requiresAuthentication']),
stored: serializer.fromJson<bool>(json['stored']), stored: serializer.fromJson<bool>(json['stored']),
isDraftMedia: serializer.fromJson<bool>(json['isDraftMedia']), isDraftMedia: serializer.fromJson<bool>(json['isDraftMedia']),
preProgressingProcess:
serializer.fromJson<int?>(json['preProgressingProcess']),
reuploadRequestedBy: reuploadRequestedBy:
serializer.fromJson<List<int>?>(json['reuploadRequestedBy']), serializer.fromJson<List<int>?>(json['reuploadRequestedBy']),
displayLimitInMilliseconds: displayLimitInMilliseconds:
@ -2349,6 +2374,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
'requiresAuthentication': serializer.toJson<bool>(requiresAuthentication), 'requiresAuthentication': serializer.toJson<bool>(requiresAuthentication),
'stored': serializer.toJson<bool>(stored), 'stored': serializer.toJson<bool>(stored),
'isDraftMedia': serializer.toJson<bool>(isDraftMedia), 'isDraftMedia': serializer.toJson<bool>(isDraftMedia),
'preProgressingProcess': serializer.toJson<int?>(preProgressingProcess),
'reuploadRequestedBy': serializer.toJson<List<int>?>(reuploadRequestedBy), 'reuploadRequestedBy': serializer.toJson<List<int>?>(reuploadRequestedBy),
'displayLimitInMilliseconds': 'displayLimitInMilliseconds':
serializer.toJson<int?>(displayLimitInMilliseconds), serializer.toJson<int?>(displayLimitInMilliseconds),
@ -2370,6 +2396,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
bool? requiresAuthentication, bool? requiresAuthentication,
bool? stored, bool? stored,
bool? isDraftMedia, bool? isDraftMedia,
Value<int?> preProgressingProcess = const Value.absent(),
Value<List<int>?> reuploadRequestedBy = const Value.absent(), Value<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(), Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<bool?> removeAudio = const Value.absent(), Value<bool?> removeAudio = const Value.absent(),
@ -2389,6 +2416,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
requiresAuthentication ?? this.requiresAuthentication, requiresAuthentication ?? this.requiresAuthentication,
stored: stored ?? this.stored, stored: stored ?? this.stored,
isDraftMedia: isDraftMedia ?? this.isDraftMedia, isDraftMedia: isDraftMedia ?? this.isDraftMedia,
preProgressingProcess: preProgressingProcess.present
? preProgressingProcess.value
: this.preProgressingProcess,
reuploadRequestedBy: reuploadRequestedBy.present reuploadRequestedBy: reuploadRequestedBy.present
? reuploadRequestedBy.value ? reuploadRequestedBy.value
: this.reuploadRequestedBy, : this.reuploadRequestedBy,
@ -2425,6 +2455,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
isDraftMedia: data.isDraftMedia.present isDraftMedia: data.isDraftMedia.present
? data.isDraftMedia.value ? data.isDraftMedia.value
: this.isDraftMedia, : this.isDraftMedia,
preProgressingProcess: data.preProgressingProcess.present
? data.preProgressingProcess.value
: this.preProgressingProcess,
reuploadRequestedBy: data.reuploadRequestedBy.present reuploadRequestedBy: data.reuploadRequestedBy.present
? data.reuploadRequestedBy.value ? data.reuploadRequestedBy.value
: this.reuploadRequestedBy, : this.reuploadRequestedBy,
@ -2462,6 +2495,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
..write('requiresAuthentication: $requiresAuthentication, ') ..write('requiresAuthentication: $requiresAuthentication, ')
..write('stored: $stored, ') ..write('stored: $stored, ')
..write('isDraftMedia: $isDraftMedia, ') ..write('isDraftMedia: $isDraftMedia, ')
..write('preProgressingProcess: $preProgressingProcess, ')
..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ')
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
..write('removeAudio: $removeAudio, ') ..write('removeAudio: $removeAudio, ')
@ -2484,6 +2518,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
requiresAuthentication, requiresAuthentication,
stored, stored,
isDraftMedia, isDraftMedia,
preProgressingProcess,
reuploadRequestedBy, reuploadRequestedBy,
displayLimitInMilliseconds, displayLimitInMilliseconds,
removeAudio, removeAudio,
@ -2504,6 +2539,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
other.requiresAuthentication == this.requiresAuthentication && other.requiresAuthentication == this.requiresAuthentication &&
other.stored == this.stored && other.stored == this.stored &&
other.isDraftMedia == this.isDraftMedia && other.isDraftMedia == this.isDraftMedia &&
other.preProgressingProcess == this.preProgressingProcess &&
other.reuploadRequestedBy == this.reuploadRequestedBy && other.reuploadRequestedBy == this.reuploadRequestedBy &&
other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && other.displayLimitInMilliseconds == this.displayLimitInMilliseconds &&
other.removeAudio == this.removeAudio && other.removeAudio == this.removeAudio &&
@ -2525,6 +2561,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
final Value<bool> requiresAuthentication; final Value<bool> requiresAuthentication;
final Value<bool> stored; final Value<bool> stored;
final Value<bool> isDraftMedia; final Value<bool> isDraftMedia;
final Value<int?> preProgressingProcess;
final Value<List<int>?> reuploadRequestedBy; final Value<List<int>?> reuploadRequestedBy;
final Value<int?> displayLimitInMilliseconds; final Value<int?> displayLimitInMilliseconds;
final Value<bool?> removeAudio; final Value<bool?> removeAudio;
@ -2543,6 +2580,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.requiresAuthentication = const Value.absent(), this.requiresAuthentication = const Value.absent(),
this.stored = const Value.absent(), this.stored = const Value.absent(),
this.isDraftMedia = const Value.absent(), this.isDraftMedia = const Value.absent(),
this.preProgressingProcess = const Value.absent(),
this.reuploadRequestedBy = const Value.absent(), this.reuploadRequestedBy = const Value.absent(),
this.displayLimitInMilliseconds = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(),
this.removeAudio = const Value.absent(), this.removeAudio = const Value.absent(),
@ -2562,6 +2600,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.requiresAuthentication = const Value.absent(), this.requiresAuthentication = const Value.absent(),
this.stored = const Value.absent(), this.stored = const Value.absent(),
this.isDraftMedia = const Value.absent(), this.isDraftMedia = const Value.absent(),
this.preProgressingProcess = const Value.absent(),
this.reuploadRequestedBy = const Value.absent(), this.reuploadRequestedBy = const Value.absent(),
this.displayLimitInMilliseconds = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(),
this.removeAudio = const Value.absent(), this.removeAudio = const Value.absent(),
@ -2582,6 +2621,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Expression<bool>? requiresAuthentication, Expression<bool>? requiresAuthentication,
Expression<bool>? stored, Expression<bool>? stored,
Expression<bool>? isDraftMedia, Expression<bool>? isDraftMedia,
Expression<int>? preProgressingProcess,
Expression<String>? reuploadRequestedBy, Expression<String>? reuploadRequestedBy,
Expression<int>? displayLimitInMilliseconds, Expression<int>? displayLimitInMilliseconds,
Expression<bool>? removeAudio, Expression<bool>? removeAudio,
@ -2602,6 +2642,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
'requires_authentication': requiresAuthentication, 'requires_authentication': requiresAuthentication,
if (stored != null) 'stored': stored, if (stored != null) 'stored': stored,
if (isDraftMedia != null) 'is_draft_media': isDraftMedia, if (isDraftMedia != null) 'is_draft_media': isDraftMedia,
if (preProgressingProcess != null)
'pre_progressing_process': preProgressingProcess,
if (reuploadRequestedBy != null) if (reuploadRequestedBy != null)
'reupload_requested_by': reuploadRequestedBy, 'reupload_requested_by': reuploadRequestedBy,
if (displayLimitInMilliseconds != null) if (displayLimitInMilliseconds != null)
@ -2625,6 +2667,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Value<bool>? requiresAuthentication, Value<bool>? requiresAuthentication,
Value<bool>? stored, Value<bool>? stored,
Value<bool>? isDraftMedia, Value<bool>? isDraftMedia,
Value<int?>? preProgressingProcess,
Value<List<int>?>? reuploadRequestedBy, Value<List<int>?>? reuploadRequestedBy,
Value<int?>? displayLimitInMilliseconds, Value<int?>? displayLimitInMilliseconds,
Value<bool?>? removeAudio, Value<bool?>? removeAudio,
@ -2644,6 +2687,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
requiresAuthentication ?? this.requiresAuthentication, requiresAuthentication ?? this.requiresAuthentication,
stored: stored ?? this.stored, stored: stored ?? this.stored,
isDraftMedia: isDraftMedia ?? this.isDraftMedia, isDraftMedia: isDraftMedia ?? this.isDraftMedia,
preProgressingProcess:
preProgressingProcess ?? this.preProgressingProcess,
reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy,
displayLimitInMilliseconds: displayLimitInMilliseconds:
displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, displayLimitInMilliseconds ?? this.displayLimitInMilliseconds,
@ -2686,6 +2731,10 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
if (isDraftMedia.present) { if (isDraftMedia.present) {
map['is_draft_media'] = Variable<bool>(isDraftMedia.value); map['is_draft_media'] = Variable<bool>(isDraftMedia.value);
} }
if (preProgressingProcess.present) {
map['pre_progressing_process'] =
Variable<int>(preProgressingProcess.value);
}
if (reuploadRequestedBy.present) { if (reuploadRequestedBy.present) {
map['reupload_requested_by'] = Variable<String>($MediaFilesTable map['reupload_requested_by'] = Variable<String>($MediaFilesTable
.$converterreuploadRequestedByn .$converterreuploadRequestedByn
@ -2732,6 +2781,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
..write('requiresAuthentication: $requiresAuthentication, ') ..write('requiresAuthentication: $requiresAuthentication, ')
..write('stored: $stored, ') ..write('stored: $stored, ')
..write('isDraftMedia: $isDraftMedia, ') ..write('isDraftMedia: $isDraftMedia, ')
..write('preProgressingProcess: $preProgressingProcess, ')
..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ')
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
..write('removeAudio: $removeAudio, ') ..write('removeAudio: $removeAudio, ')
@ -8902,6 +8952,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({
Value<bool> requiresAuthentication, Value<bool> requiresAuthentication,
Value<bool> stored, Value<bool> stored,
Value<bool> isDraftMedia, Value<bool> isDraftMedia,
Value<int?> preProgressingProcess,
Value<List<int>?> reuploadRequestedBy, Value<List<int>?> reuploadRequestedBy,
Value<int?> displayLimitInMilliseconds, Value<int?> displayLimitInMilliseconds,
Value<bool?> removeAudio, Value<bool?> removeAudio,
@ -8921,6 +8972,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({
Value<bool> requiresAuthentication, Value<bool> requiresAuthentication,
Value<bool> stored, Value<bool> stored,
Value<bool> isDraftMedia, Value<bool> isDraftMedia,
Value<int?> preProgressingProcess,
Value<List<int>?> reuploadRequestedBy, Value<List<int>?> reuploadRequestedBy,
Value<int?> displayLimitInMilliseconds, Value<int?> displayLimitInMilliseconds,
Value<bool?> removeAudio, Value<bool?> removeAudio,
@ -8990,6 +9042,10 @@ class $$MediaFilesTableFilterComposer
ColumnFilters<bool> get isDraftMedia => $composableBuilder( ColumnFilters<bool> get isDraftMedia => $composableBuilder(
column: $table.isDraftMedia, builder: (column) => ColumnFilters(column)); column: $table.isDraftMedia, builder: (column) => ColumnFilters(column));
ColumnFilters<int> get preProgressingProcess => $composableBuilder(
column: $table.preProgressingProcess,
builder: (column) => ColumnFilters(column));
ColumnWithTypeConverterFilters<List<int>?, List<int>, String> ColumnWithTypeConverterFilters<List<int>?, List<int>, String>
get reuploadRequestedBy => $composableBuilder( get reuploadRequestedBy => $composableBuilder(
column: $table.reuploadRequestedBy, column: $table.reuploadRequestedBy,
@ -9077,6 +9133,10 @@ class $$MediaFilesTableOrderingComposer
column: $table.isDraftMedia, column: $table.isDraftMedia,
builder: (column) => ColumnOrderings(column)); builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get preProgressingProcess => $composableBuilder(
column: $table.preProgressingProcess,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get reuploadRequestedBy => $composableBuilder( ColumnOrderings<String> get reuploadRequestedBy => $composableBuilder(
column: $table.reuploadRequestedBy, column: $table.reuploadRequestedBy,
builder: (column) => ColumnOrderings(column)); builder: (column) => ColumnOrderings(column));
@ -9144,6 +9204,9 @@ class $$MediaFilesTableAnnotationComposer
GeneratedColumn<bool> get isDraftMedia => $composableBuilder( GeneratedColumn<bool> get isDraftMedia => $composableBuilder(
column: $table.isDraftMedia, builder: (column) => column); column: $table.isDraftMedia, builder: (column) => column);
GeneratedColumn<int> get preProgressingProcess => $composableBuilder(
column: $table.preProgressingProcess, builder: (column) => column);
GeneratedColumnWithTypeConverter<List<int>?, String> GeneratedColumnWithTypeConverter<List<int>?, String>
get reuploadRequestedBy => $composableBuilder( get reuploadRequestedBy => $composableBuilder(
column: $table.reuploadRequestedBy, builder: (column) => column); column: $table.reuploadRequestedBy, builder: (column) => column);
@ -9224,6 +9287,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
Value<bool> requiresAuthentication = const Value.absent(), Value<bool> requiresAuthentication = const Value.absent(),
Value<bool> stored = const Value.absent(), Value<bool> stored = const Value.absent(),
Value<bool> isDraftMedia = const Value.absent(), Value<bool> isDraftMedia = const Value.absent(),
Value<int?> preProgressingProcess = const Value.absent(),
Value<List<int>?> reuploadRequestedBy = const Value.absent(), Value<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(), Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<bool?> removeAudio = const Value.absent(), Value<bool?> removeAudio = const Value.absent(),
@ -9243,6 +9307,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
requiresAuthentication: requiresAuthentication, requiresAuthentication: requiresAuthentication,
stored: stored, stored: stored,
isDraftMedia: isDraftMedia, isDraftMedia: isDraftMedia,
preProgressingProcess: preProgressingProcess,
reuploadRequestedBy: reuploadRequestedBy, reuploadRequestedBy: reuploadRequestedBy,
displayLimitInMilliseconds: displayLimitInMilliseconds, displayLimitInMilliseconds: displayLimitInMilliseconds,
removeAudio: removeAudio, removeAudio: removeAudio,
@ -9262,6 +9327,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
Value<bool> requiresAuthentication = const Value.absent(), Value<bool> requiresAuthentication = const Value.absent(),
Value<bool> stored = const Value.absent(), Value<bool> stored = const Value.absent(),
Value<bool> isDraftMedia = const Value.absent(), Value<bool> isDraftMedia = const Value.absent(),
Value<int?> preProgressingProcess = const Value.absent(),
Value<List<int>?> reuploadRequestedBy = const Value.absent(), Value<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(), Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<bool?> removeAudio = const Value.absent(), Value<bool?> removeAudio = const Value.absent(),
@ -9281,6 +9347,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
requiresAuthentication: requiresAuthentication, requiresAuthentication: requiresAuthentication,
stored: stored, stored: stored,
isDraftMedia: isDraftMedia, isDraftMedia: isDraftMedia,
preProgressingProcess: preProgressingProcess,
reuploadRequestedBy: reuploadRequestedBy, reuploadRequestedBy: reuploadRequestedBy,
displayLimitInMilliseconds: displayLimitInMilliseconds, displayLimitInMilliseconds: displayLimitInMilliseconds,
removeAudio: removeAudio, removeAudio: removeAudio,

View file

@ -4303,6 +4303,391 @@ i1.GeneratedColumn<int> _column_206(String aliasedName) =>
i1.GeneratedColumn<int>( i1.GeneratedColumn<int>(
'new_delete_messages_after_milliseconds', aliasedName, true, 'new_delete_messages_after_milliseconds', aliasedName, true,
type: i1.DriftSqlType.int, $customConstraints: 'NULL'); type: i1.DriftSqlType.int, $customConstraints: 'NULL');
final class Schema9 extends i0.VersionedSchema {
Schema9({required super.database}) : super(version: 9);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
groups,
mediaFiles,
messages,
messageHistories,
reactions,
groupMembers,
receipts,
receivedReceipts,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
messageActions,
groupHistories,
];
late final Shape22 contacts = Shape22(
source: i0.VersionedTable(
entityName: 'contacts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(user_id)',
],
columns: [
_column_106,
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
],
attachedDatabase: database,
),
alias: null);
late final Shape23 groups = Shape23(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_id)',
],
columns: [
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
_column_130,
_column_131,
_column_132,
_column_133,
_column_134,
_column_118,
_column_135,
_column_136,
_column_137,
_column_138,
_column_139,
_column_140,
_column_141,
_column_142,
],
attachedDatabase: database,
),
alias: null);
late final Shape36 mediaFiles = Shape36(
source: i0.VersionedTable(
entityName: 'media_files',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(media_id)',
],
columns: [
_column_143,
_column_144,
_column_145,
_column_146,
_column_147,
_column_148,
_column_149,
_column_207,
_column_150,
_column_151,
_column_152,
_column_153,
_column_154,
_column_155,
_column_156,
_column_157,
_column_118,
],
attachedDatabase: database,
),
alias: null);
late final Shape25 messages = Shape25(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id)',
],
columns: [
_column_158,
_column_159,
_column_160,
_column_144,
_column_161,
_column_162,
_column_163,
_column_164,
_column_165,
_column_153,
_column_166,
_column_167,
_column_168,
_column_169,
_column_118,
_column_170,
_column_171,
_column_172,
],
attachedDatabase: database,
),
alias: null);
late final Shape26 messageHistories = Shape26(
source: i0.VersionedTable(
entityName: 'message_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_173,
_column_174,
_column_175,
_column_161,
_column_118,
],
attachedDatabase: database,
),
alias: null);
late final Shape27 reactions = Shape27(
source: i0.VersionedTable(
entityName: 'reactions',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id, sender_id, emoji)',
],
columns: [
_column_174,
_column_176,
_column_177,
_column_118,
],
attachedDatabase: database,
),
alias: null);
late final Shape28 groupMembers = Shape28(
source: i0.VersionedTable(
entityName: 'group_members',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_id, contact_id)',
],
columns: [
_column_158,
_column_178,
_column_179,
_column_180,
_column_181,
_column_118,
],
attachedDatabase: database,
),
alias: null);
late final Shape29 receipts = Shape29(
source: i0.VersionedTable(
entityName: 'receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(receipt_id)',
],
columns: [
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
_column_187,
_column_188,
_column_189,
_column_190,
_column_191,
_column_118,
],
attachedDatabase: database,
),
alias: null);
late final Shape30 receivedReceipts = Shape30(
source: i0.VersionedTable(
entityName: 'received_receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(receipt_id)',
],
columns: [
_column_182,
_column_118,
],
attachedDatabase: database,
),
alias: null);
late final Shape31 signalIdentityKeyStores = Shape31(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_192,
_column_193,
_column_194,
_column_118,
],
attachedDatabase: database,
),
alias: null);
late final Shape32 signalPreKeyStores = Shape32(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(pre_key_id)',
],
columns: [
_column_195,
_column_196,
_column_118,
],
attachedDatabase: database,
),
alias: null);
late final Shape11 signalSenderKeyStores = Shape11(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(sender_key_name)',
],
columns: [
_column_197,
_column_198,
],
attachedDatabase: database,
),
alias: null);
late final Shape33 signalSessionStores = Shape33(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_192,
_column_193,
_column_199,
_column_118,
],
attachedDatabase: database,
),
alias: null);
late final Shape34 messageActions = Shape34(
source: i0.VersionedTable(
entityName: 'message_actions',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id, contact_id, type)',
],
columns: [
_column_174,
_column_183,
_column_144,
_column_200,
],
attachedDatabase: database,
),
alias: null);
late final Shape35 groupHistories = Shape35(
source: i0.VersionedTable(
entityName: 'group_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_history_id)',
],
columns: [
_column_201,
_column_158,
_column_202,
_column_203,
_column_204,
_column_205,
_column_206,
_column_144,
_column_200,
],
attachedDatabase: database,
),
alias: null);
}
class Shape36 extends i0.VersionedTable {
Shape36({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get mediaId =>
columnsByName['media_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get type =>
columnsByName['type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get uploadState =>
columnsByName['upload_state']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get downloadState =>
columnsByName['download_state']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get requiresAuthentication =>
columnsByName['requires_authentication']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get stored =>
columnsByName['stored']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get isDraftMedia =>
columnsByName['is_draft_media']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get preProgressingProcess =>
columnsByName['pre_progressing_process']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get reuploadRequestedBy =>
columnsByName['reupload_requested_by']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get displayLimitInMilliseconds =>
columnsByName['display_limit_in_milliseconds']!
as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get removeAudio =>
columnsByName['remove_audio']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<i2.Uint8List> get downloadToken =>
columnsByName['download_token']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get encryptionKey =>
columnsByName['encryption_key']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get encryptionMac =>
columnsByName['encryption_mac']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get encryptionNonce =>
columnsByName['encryption_nonce']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get storedFileHash =>
columnsByName['stored_file_hash']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<int> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_207(String aliasedName) =>
i1.GeneratedColumn<int>('pre_progressing_process', aliasedName, true,
type: i1.DriftSqlType.int, $customConstraints: 'NULL');
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -4311,6 +4696,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6, required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -4349,6 +4735,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from7To8(migrator, schema); await from7To8(migrator, schema);
return 8; return 8;
case 8:
final schema = Schema9(database: database);
final migrator = i1.Migrator(database, schema);
await from8To9(migrator, schema);
return 9;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -4363,6 +4754,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6, required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
}) => }) =>
i0.VersionedSchema.stepByStepHelper( i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
@ -4373,4 +4765,5 @@ i1.OnUpgrade stepByStep({
from5To6: from5To6, from5To6: from5To6,
from6To7: from6To7, from6To7: from6To7,
from7To8: from7To8, from7To8: from7To8,
from8To9: from8To9,
)); ));

View file

@ -1,14 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:pro_video_editor/pro_video_editor.dart'; import 'package:pro_video_editor/pro_video_editor.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/channels/video_compression.channel.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:video_compress/video_compress.dart';
Future<void> compressImage( Future<void> compressImage(
File sourceFile, File sourceFile,
@ -16,8 +17,6 @@ Future<void> compressImage(
) async { ) async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
// // ffmpeg -i input.png -vcodec libwebp -lossless 1 -preset default output.webp
try { try {
var compressedBytes = await FlutterImageCompress.compressWithFile( var compressedBytes = await FlutterImageCompress.compressWithFile(
sourceFile.path, sourceFile.path,
@ -74,37 +73,53 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
try { try {
final task = VideoRenderData( final task = VideoRenderData(
video: EditorVideo.file(media.originalPath), video: EditorVideo.file(media.originalPath),
// qualityPreset: VideoQualityPreset.p720High,
imageBytes: media.overlayImagePath.readAsBytesSync(), imageBytes: media.overlayImagePath.readAsBytesSync(),
enableAudio: !media.removeAudio, enableAudio: !media.removeAudio,
); );
final result = await ProVideoEditor.instance.renderVideo(task); await ProVideoEditor.instance
media.ffmpegOutputPath.writeAsBytesSync(result); .renderVideoToFile(media.ffmpegOutputPath.path, task);
MediaInfo? mediaInfo; if (Platform.isIOS ||
try { media.ffmpegOutputPath.statSync().size >= 10_000_000 ||
mediaInfo = await VideoCompress.compressVideo( !kReleaseMode) {
media.ffmpegOutputPath.path, String? compressedPath;
quality: VideoQuality.Res640x480Quality, try {
includeAudio: true, compressedPath = await VideoCompressionChannel.compressVideo(
); inputPath: media.ffmpegOutputPath.path,
Log.info('Video has now size of ${mediaInfo!.filesize} bytes.'); outputPath: media.tempPath.path,
} catch (e) { onProgress: (progress) async {
Log.error('during video compression: $e'); await twonlyDB.mediaFilesDao.updateMedia(
} media.mediaFile.mediaId,
MediaFilesCompanion(
preProgressingProcess: Value((progress * 100).toInt()),
),
);
},
);
} catch (e) {
Log.error('during video compression: $e');
}
if (mediaInfo == null) { if (compressedPath == null) {
Log.error('Could not compress video using original video.'); Log.error('Could not compress video using original video.');
// as a fall back use the non compressed version // as a fall back use the non compressed version
media.ffmpegOutputPath.copySync(media.tempPath.path); media.ffmpegOutputPath.copySync(media.tempPath.path);
}
} else { } else {
mediaInfo.file!.copySync(media.tempPath.path); // In case the video is smaller than 10MB do not compress it...
media.ffmpegOutputPath.copySync(media.tempPath.path);
} }
stopwatch.stop(); stopwatch.stop();
final sizeFrom = (media.ffmpegOutputPath.statSync().size / 1024 / 1024)
.toStringAsFixed(2);
final sizeTo =
(media.tempPath.statSync().size / 1024 / 1024).toStringAsFixed(2);
Log.info( Log.info(
'It took ${stopwatch.elapsedMilliseconds}ms to compress the video. Reduced from ${media.ffmpegOutputPath.statSync().size} to ${media.tempPath.statSync().size} bytes.', 'It took ${stopwatch.elapsedMilliseconds}ms to compress the video. Reduced from $sizeFrom to $sizeTo bytes.',
); );
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);

View file

@ -166,10 +166,17 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
onTap = () => context.push(Routes.settingsSubscription); onTap = () => context.push(Routes.settingsSubscription);
} }
if (mediaFile.uploadState == UploadState.preprocessing || if (mediaFile.uploadState == UploadState.initialized) {
mediaFile.uploadState == UploadState.initialized) {
text = context.lang.inProcess; text = context.lang.inProcess;
} }
if (mediaFile.uploadState == UploadState.preprocessing) {
final progress = mediaFile.preProgressingProcess ?? 0;
if (progress > 0) {
text = '${context.lang.inProcess} ($progress%)';
} else {
text = context.lang.inProcess;
}
}
} }
hasLoader = true; hasLoader = true;

View file

@ -1940,14 +1940,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.2.0" version: "10.2.0"
video_compress:
dependency: "direct main"
description:
name: video_compress
sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
video_player: video_player:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -106,7 +106,6 @@ dependencies:
gal: ^2.3.1 gal: ^2.3.1
google_mlkit_barcode_scanning: ^0.14.1 google_mlkit_barcode_scanning: ^0.14.1
pro_video_editor: ^1.6.1 pro_video_editor: ^1.6.1
video_compress: ^3.1.4
dependency_overrides: dependency_overrides:
dots_indicator: dots_indicator:

View file

@ -12,6 +12,7 @@ import 'schema_v5.dart' as v5;
import 'schema_v6.dart' as v6; import 'schema_v6.dart' as v6;
import 'schema_v7.dart' as v7; import 'schema_v7.dart' as v7;
import 'schema_v8.dart' as v8; import 'schema_v8.dart' as v8;
import 'schema_v9.dart' as v9;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -33,10 +34,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v7.DatabaseAtV7(db); return v7.DatabaseAtV7(db);
case 8: case 8:
return v8.DatabaseAtV8(db); return v8.DatabaseAtV8(db);
case 9:
return v9.DatabaseAtV9(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8]; static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9];
} }

File diff suppressed because it is too large Load diff