mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-18 14:42:54 +00:00
Improve: Video compression with progress updates
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
31caa133dc
commit
c85914672e
22 changed files with 9494 additions and 136 deletions
|
|
@ -3,6 +3,7 @@
|
|||
## 0.0.98
|
||||
|
||||
- Fix: Issue with contact requests
|
||||
- Improve: Video compression with progress updates
|
||||
|
||||
## 0.0.96
|
||||
|
||||
|
|
|
|||
|
|
@ -72,4 +72,5 @@ flutter {
|
|||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||
implementation 'com.otaliastudios:transcoder:0.11.0'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,14 @@
|
|||
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 android.view.KeyEvent
|
||||
import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownPlugin.eventSink
|
||||
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
import android.view.KeyEvent.KEYCODE_VOLUME_UP
|
||||
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() {
|
||||
|
||||
private val MEDIA_STORE_CHANNEL = "eu.twonly/mediaStore"
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) {
|
||||
eventSink!!.success(true)
|
||||
|
|
@ -37,80 +24,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, MEDIA_STORE_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 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
|
||||
MediaStoreChannel.configure(flutterEngine, applicationContext)
|
||||
VideoCompressionChannel.configure(flutterEngine, applicationContext)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
92
android/app/src/main/kotlin/eu/twonly/MediaStoreChannel.kt
Normal file
92
android/app/src/main/kotlin/eu/twonly/MediaStoreChannel.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package eu.twonly
|
||||
|
||||
class MyMediaStorageProxy {
|
||||
|
||||
|
||||
}
|
||||
107
android/app/src/main/kotlin/eu/twonly/VideoCompressionChannel.kt
Normal file
107
android/app/src/main/kotlin/eu/twonly/VideoCompressionChannel.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -316,8 +316,6 @@ PODS:
|
|||
- SwiftyGif (5.4.5)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_compress (0.3.0):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
|
|
@ -366,7 +364,6 @@ DEPENDENCIES:
|
|||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
- SwiftProtobuf
|
||||
- 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`)
|
||||
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
|
||||
|
||||
|
|
@ -473,8 +470,6 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_compress:
|
||||
:path: ".symlinks/plugins/video_compress/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
workmanager_apple:
|
||||
|
|
@ -545,7 +540,6 @@ SPEC CHECKSUMS:
|
|||
SwiftProtobuf: c901f00a3e125dc33cac9b16824da85682ee47da
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
video_compress: f2133a07762889d67f0711ac831faa26f956980e
|
||||
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
||||
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
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 */; };
|
||||
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 */; };
|
||||
/* 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; };
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
|
|
@ -235,6 +237,7 @@
|
|||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */,
|
||||
D24E27CC2F38ABC10055D9D1 /* RunnerRelease.entitlements */,
|
||||
D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */,
|
||||
D2265DD42D920142000D99BB /* Runner.entitlements */,
|
||||
|
|
@ -624,6 +627,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */,
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ import flutter_sharing_intent
|
|||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
|
||||
if let registrar = self.registrar(forPlugin: "VideoCompressionChannel") {
|
||||
VideoCompressionChannel.register(with: registrar.messenger())
|
||||
}
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
|
|
|
|||
253
ios/Runner/VideoCompressionChannel.swift
Normal file
253
ios/Runner/VideoCompressionChannel.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
45
lib/src/channels/video_compression.channel.dart
Normal file
45
lib/src/channels/video_compression.channel.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2061
lib/src/database/schemas/twonly_db/drift_schema_v9.json
Normal file
2061
lib/src/database/schemas/twonly_db/drift_schema_v9.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -48,6 +48,8 @@ class MediaFiles extends Table {
|
|||
BoolColumn get stored => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))();
|
||||
|
||||
IntColumn get preProgressingProcess => integer().nullable()();
|
||||
|
||||
TextColumn get reuploadRequestedBy =>
|
||||
text().map(IntListTypeConverter()).nullable()();
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 8;
|
||||
int get schemaVersion => 9;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
|
|
@ -131,6 +131,12 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
// ignore: experimental_member_use
|
||||
await m.alterTable(TableMigration(schema.messageActions));
|
||||
},
|
||||
from8To9: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.mediaFiles,
|
||||
schema.mediaFiles.preProgressingProcess,
|
||||
);
|
||||
},
|
||||
)(m, from, to);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1950,6 +1950,12 @@ class $MediaFilesTable extends MediaFiles
|
|||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_draft_media" IN (0, 1))'),
|
||||
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
|
||||
late final GeneratedColumnWithTypeConverter<List<int>?, String>
|
||||
reuploadRequestedBy = GeneratedColumn<String>(
|
||||
|
|
@ -2019,6 +2025,7 @@ class $MediaFilesTable extends MediaFiles
|
|||
requiresAuthentication,
|
||||
stored,
|
||||
isDraftMedia,
|
||||
preProgressingProcess,
|
||||
reuploadRequestedBy,
|
||||
displayLimitInMilliseconds,
|
||||
removeAudio,
|
||||
|
|
@ -2061,6 +2068,12 @@ class $MediaFilesTable extends MediaFiles
|
|||
isDraftMedia.isAcceptableOrUnknown(
|
||||
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')) {
|
||||
context.handle(
|
||||
_displayLimitInMillisecondsMeta,
|
||||
|
|
@ -2134,6 +2147,8 @@ class $MediaFilesTable extends MediaFiles
|
|||
.read(DriftSqlType.bool, data['${effectivePrefix}stored'])!,
|
||||
isDraftMedia: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.bool, data['${effectivePrefix}is_draft_media'])!,
|
||||
preProgressingProcess: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int, data['${effectivePrefix}pre_progressing_process']),
|
||||
reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn
|
||||
.fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string,
|
||||
data['${effectivePrefix}reupload_requested_by'])),
|
||||
|
|
@ -2189,6 +2204,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
final bool requiresAuthentication;
|
||||
final bool stored;
|
||||
final bool isDraftMedia;
|
||||
final int? preProgressingProcess;
|
||||
final List<int>? reuploadRequestedBy;
|
||||
final int? displayLimitInMilliseconds;
|
||||
final bool? removeAudio;
|
||||
|
|
@ -2206,6 +2222,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
required this.requiresAuthentication,
|
||||
required this.stored,
|
||||
required this.isDraftMedia,
|
||||
this.preProgressingProcess,
|
||||
this.reuploadRequestedBy,
|
||||
this.displayLimitInMilliseconds,
|
||||
this.removeAudio,
|
||||
|
|
@ -2234,6 +2251,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
map['requires_authentication'] = Variable<bool>(requiresAuthentication);
|
||||
map['stored'] = Variable<bool>(stored);
|
||||
map['is_draft_media'] = Variable<bool>(isDraftMedia);
|
||||
if (!nullToAbsent || preProgressingProcess != null) {
|
||||
map['pre_progressing_process'] = Variable<int>(preProgressingProcess);
|
||||
}
|
||||
if (!nullToAbsent || reuploadRequestedBy != null) {
|
||||
map['reupload_requested_by'] = Variable<String>($MediaFilesTable
|
||||
.$converterreuploadRequestedByn
|
||||
|
|
@ -2278,6 +2298,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
requiresAuthentication: Value(requiresAuthentication),
|
||||
stored: Value(stored),
|
||||
isDraftMedia: Value(isDraftMedia),
|
||||
preProgressingProcess: preProgressingProcess == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(preProgressingProcess),
|
||||
reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(reuploadRequestedBy),
|
||||
|
|
@ -2322,6 +2345,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
serializer.fromJson<bool>(json['requiresAuthentication']),
|
||||
stored: serializer.fromJson<bool>(json['stored']),
|
||||
isDraftMedia: serializer.fromJson<bool>(json['isDraftMedia']),
|
||||
preProgressingProcess:
|
||||
serializer.fromJson<int?>(json['preProgressingProcess']),
|
||||
reuploadRequestedBy:
|
||||
serializer.fromJson<List<int>?>(json['reuploadRequestedBy']),
|
||||
displayLimitInMilliseconds:
|
||||
|
|
@ -2349,6 +2374,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
'requiresAuthentication': serializer.toJson<bool>(requiresAuthentication),
|
||||
'stored': serializer.toJson<bool>(stored),
|
||||
'isDraftMedia': serializer.toJson<bool>(isDraftMedia),
|
||||
'preProgressingProcess': serializer.toJson<int?>(preProgressingProcess),
|
||||
'reuploadRequestedBy': serializer.toJson<List<int>?>(reuploadRequestedBy),
|
||||
'displayLimitInMilliseconds':
|
||||
serializer.toJson<int?>(displayLimitInMilliseconds),
|
||||
|
|
@ -2370,6 +2396,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
bool? requiresAuthentication,
|
||||
bool? stored,
|
||||
bool? isDraftMedia,
|
||||
Value<int?> preProgressingProcess = const Value.absent(),
|
||||
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
||||
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
||||
Value<bool?> removeAudio = const Value.absent(),
|
||||
|
|
@ -2389,6 +2416,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
requiresAuthentication ?? this.requiresAuthentication,
|
||||
stored: stored ?? this.stored,
|
||||
isDraftMedia: isDraftMedia ?? this.isDraftMedia,
|
||||
preProgressingProcess: preProgressingProcess.present
|
||||
? preProgressingProcess.value
|
||||
: this.preProgressingProcess,
|
||||
reuploadRequestedBy: reuploadRequestedBy.present
|
||||
? reuploadRequestedBy.value
|
||||
: this.reuploadRequestedBy,
|
||||
|
|
@ -2425,6 +2455,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
isDraftMedia: data.isDraftMedia.present
|
||||
? data.isDraftMedia.value
|
||||
: this.isDraftMedia,
|
||||
preProgressingProcess: data.preProgressingProcess.present
|
||||
? data.preProgressingProcess.value
|
||||
: this.preProgressingProcess,
|
||||
reuploadRequestedBy: data.reuploadRequestedBy.present
|
||||
? data.reuploadRequestedBy.value
|
||||
: this.reuploadRequestedBy,
|
||||
|
|
@ -2462,6 +2495,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
..write('requiresAuthentication: $requiresAuthentication, ')
|
||||
..write('stored: $stored, ')
|
||||
..write('isDraftMedia: $isDraftMedia, ')
|
||||
..write('preProgressingProcess: $preProgressingProcess, ')
|
||||
..write('reuploadRequestedBy: $reuploadRequestedBy, ')
|
||||
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
|
||||
..write('removeAudio: $removeAudio, ')
|
||||
|
|
@ -2484,6 +2518,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
requiresAuthentication,
|
||||
stored,
|
||||
isDraftMedia,
|
||||
preProgressingProcess,
|
||||
reuploadRequestedBy,
|
||||
displayLimitInMilliseconds,
|
||||
removeAudio,
|
||||
|
|
@ -2504,6 +2539,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
other.requiresAuthentication == this.requiresAuthentication &&
|
||||
other.stored == this.stored &&
|
||||
other.isDraftMedia == this.isDraftMedia &&
|
||||
other.preProgressingProcess == this.preProgressingProcess &&
|
||||
other.reuploadRequestedBy == this.reuploadRequestedBy &&
|
||||
other.displayLimitInMilliseconds == this.displayLimitInMilliseconds &&
|
||||
other.removeAudio == this.removeAudio &&
|
||||
|
|
@ -2525,6 +2561,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
final Value<bool> requiresAuthentication;
|
||||
final Value<bool> stored;
|
||||
final Value<bool> isDraftMedia;
|
||||
final Value<int?> preProgressingProcess;
|
||||
final Value<List<int>?> reuploadRequestedBy;
|
||||
final Value<int?> displayLimitInMilliseconds;
|
||||
final Value<bool?> removeAudio;
|
||||
|
|
@ -2543,6 +2580,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
this.requiresAuthentication = const Value.absent(),
|
||||
this.stored = const Value.absent(),
|
||||
this.isDraftMedia = const Value.absent(),
|
||||
this.preProgressingProcess = const Value.absent(),
|
||||
this.reuploadRequestedBy = const Value.absent(),
|
||||
this.displayLimitInMilliseconds = const Value.absent(),
|
||||
this.removeAudio = const Value.absent(),
|
||||
|
|
@ -2562,6 +2600,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
this.requiresAuthentication = const Value.absent(),
|
||||
this.stored = const Value.absent(),
|
||||
this.isDraftMedia = const Value.absent(),
|
||||
this.preProgressingProcess = const Value.absent(),
|
||||
this.reuploadRequestedBy = const Value.absent(),
|
||||
this.displayLimitInMilliseconds = const Value.absent(),
|
||||
this.removeAudio = const Value.absent(),
|
||||
|
|
@ -2582,6 +2621,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
Expression<bool>? requiresAuthentication,
|
||||
Expression<bool>? stored,
|
||||
Expression<bool>? isDraftMedia,
|
||||
Expression<int>? preProgressingProcess,
|
||||
Expression<String>? reuploadRequestedBy,
|
||||
Expression<int>? displayLimitInMilliseconds,
|
||||
Expression<bool>? removeAudio,
|
||||
|
|
@ -2602,6 +2642,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
'requires_authentication': requiresAuthentication,
|
||||
if (stored != null) 'stored': stored,
|
||||
if (isDraftMedia != null) 'is_draft_media': isDraftMedia,
|
||||
if (preProgressingProcess != null)
|
||||
'pre_progressing_process': preProgressingProcess,
|
||||
if (reuploadRequestedBy != null)
|
||||
'reupload_requested_by': reuploadRequestedBy,
|
||||
if (displayLimitInMilliseconds != null)
|
||||
|
|
@ -2625,6 +2667,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
Value<bool>? requiresAuthentication,
|
||||
Value<bool>? stored,
|
||||
Value<bool>? isDraftMedia,
|
||||
Value<int?>? preProgressingProcess,
|
||||
Value<List<int>?>? reuploadRequestedBy,
|
||||
Value<int?>? displayLimitInMilliseconds,
|
||||
Value<bool?>? removeAudio,
|
||||
|
|
@ -2644,6 +2687,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
requiresAuthentication ?? this.requiresAuthentication,
|
||||
stored: stored ?? this.stored,
|
||||
isDraftMedia: isDraftMedia ?? this.isDraftMedia,
|
||||
preProgressingProcess:
|
||||
preProgressingProcess ?? this.preProgressingProcess,
|
||||
reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy,
|
||||
displayLimitInMilliseconds:
|
||||
displayLimitInMilliseconds ?? this.displayLimitInMilliseconds,
|
||||
|
|
@ -2686,6 +2731,10 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
if (isDraftMedia.present) {
|
||||
map['is_draft_media'] = Variable<bool>(isDraftMedia.value);
|
||||
}
|
||||
if (preProgressingProcess.present) {
|
||||
map['pre_progressing_process'] =
|
||||
Variable<int>(preProgressingProcess.value);
|
||||
}
|
||||
if (reuploadRequestedBy.present) {
|
||||
map['reupload_requested_by'] = Variable<String>($MediaFilesTable
|
||||
.$converterreuploadRequestedByn
|
||||
|
|
@ -2732,6 +2781,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
..write('requiresAuthentication: $requiresAuthentication, ')
|
||||
..write('stored: $stored, ')
|
||||
..write('isDraftMedia: $isDraftMedia, ')
|
||||
..write('preProgressingProcess: $preProgressingProcess, ')
|
||||
..write('reuploadRequestedBy: $reuploadRequestedBy, ')
|
||||
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
|
||||
..write('removeAudio: $removeAudio, ')
|
||||
|
|
@ -8902,6 +8952,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({
|
|||
Value<bool> requiresAuthentication,
|
||||
Value<bool> stored,
|
||||
Value<bool> isDraftMedia,
|
||||
Value<int?> preProgressingProcess,
|
||||
Value<List<int>?> reuploadRequestedBy,
|
||||
Value<int?> displayLimitInMilliseconds,
|
||||
Value<bool?> removeAudio,
|
||||
|
|
@ -8921,6 +8972,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({
|
|||
Value<bool> requiresAuthentication,
|
||||
Value<bool> stored,
|
||||
Value<bool> isDraftMedia,
|
||||
Value<int?> preProgressingProcess,
|
||||
Value<List<int>?> reuploadRequestedBy,
|
||||
Value<int?> displayLimitInMilliseconds,
|
||||
Value<bool?> removeAudio,
|
||||
|
|
@ -8990,6 +9042,10 @@ class $$MediaFilesTableFilterComposer
|
|||
ColumnFilters<bool> get isDraftMedia => $composableBuilder(
|
||||
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>
|
||||
get reuploadRequestedBy => $composableBuilder(
|
||||
column: $table.reuploadRequestedBy,
|
||||
|
|
@ -9077,6 +9133,10 @@ class $$MediaFilesTableOrderingComposer
|
|||
column: $table.isDraftMedia,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<int> get preProgressingProcess => $composableBuilder(
|
||||
column: $table.preProgressingProcess,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get reuploadRequestedBy => $composableBuilder(
|
||||
column: $table.reuploadRequestedBy,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
|
@ -9144,6 +9204,9 @@ class $$MediaFilesTableAnnotationComposer
|
|||
GeneratedColumn<bool> get isDraftMedia => $composableBuilder(
|
||||
column: $table.isDraftMedia, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get preProgressingProcess => $composableBuilder(
|
||||
column: $table.preProgressingProcess, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<List<int>?, String>
|
||||
get reuploadRequestedBy => $composableBuilder(
|
||||
column: $table.reuploadRequestedBy, builder: (column) => column);
|
||||
|
|
@ -9224,6 +9287,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
|
|||
Value<bool> requiresAuthentication = const Value.absent(),
|
||||
Value<bool> stored = const Value.absent(),
|
||||
Value<bool> isDraftMedia = const Value.absent(),
|
||||
Value<int?> preProgressingProcess = const Value.absent(),
|
||||
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
||||
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
||||
Value<bool?> removeAudio = const Value.absent(),
|
||||
|
|
@ -9243,6 +9307,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
|
|||
requiresAuthentication: requiresAuthentication,
|
||||
stored: stored,
|
||||
isDraftMedia: isDraftMedia,
|
||||
preProgressingProcess: preProgressingProcess,
|
||||
reuploadRequestedBy: reuploadRequestedBy,
|
||||
displayLimitInMilliseconds: displayLimitInMilliseconds,
|
||||
removeAudio: removeAudio,
|
||||
|
|
@ -9262,6 +9327,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
|
|||
Value<bool> requiresAuthentication = const Value.absent(),
|
||||
Value<bool> stored = const Value.absent(),
|
||||
Value<bool> isDraftMedia = const Value.absent(),
|
||||
Value<int?> preProgressingProcess = const Value.absent(),
|
||||
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
||||
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
||||
Value<bool?> removeAudio = const Value.absent(),
|
||||
|
|
@ -9281,6 +9347,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
|
|||
requiresAuthentication: requiresAuthentication,
|
||||
stored: stored,
|
||||
isDraftMedia: isDraftMedia,
|
||||
preProgressingProcess: preProgressingProcess,
|
||||
reuploadRequestedBy: reuploadRequestedBy,
|
||||
displayLimitInMilliseconds: displayLimitInMilliseconds,
|
||||
removeAudio: removeAudio,
|
||||
|
|
|
|||
|
|
@ -4303,6 +4303,391 @@ i1.GeneratedColumn<int> _column_206(String aliasedName) =>
|
|||
i1.GeneratedColumn<int>(
|
||||
'new_delete_messages_after_milliseconds', aliasedName, true,
|
||||
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({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
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, Schema7 schema) from6To7,
|
||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
|
|
@ -4349,6 +4735,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
final migrator = i1.Migrator(database, schema);
|
||||
await from7To8(migrator, schema);
|
||||
return 8;
|
||||
case 8:
|
||||
final schema = Schema9(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from8To9(migrator, schema);
|
||||
return 9;
|
||||
default:
|
||||
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, Schema7 schema) from6To7,
|
||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||
}) =>
|
||||
i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
|
|
@ -4373,4 +4765,5 @@ i1.OnUpgrade stepByStep({
|
|||
from5To6: from5To6,
|
||||
from6To7: from6To7,
|
||||
from7To8: from7To8,
|
||||
from8To9: from8To9,
|
||||
));
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:pro_video_editor/pro_video_editor.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/twonly.db.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
Future<void> compressImage(
|
||||
File sourceFile,
|
||||
|
|
@ -16,8 +17,6 @@ Future<void> compressImage(
|
|||
) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
// // ffmpeg -i input.png -vcodec libwebp -lossless 1 -preset default output.webp
|
||||
|
||||
try {
|
||||
var compressedBytes = await FlutterImageCompress.compressWithFile(
|
||||
sourceFile.path,
|
||||
|
|
@ -74,37 +73,53 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
|
|||
try {
|
||||
final task = VideoRenderData(
|
||||
video: EditorVideo.file(media.originalPath),
|
||||
// qualityPreset: VideoQualityPreset.p720High,
|
||||
imageBytes: media.overlayImagePath.readAsBytesSync(),
|
||||
enableAudio: !media.removeAudio,
|
||||
);
|
||||
|
||||
final result = await ProVideoEditor.instance.renderVideo(task);
|
||||
media.ffmpegOutputPath.writeAsBytesSync(result);
|
||||
await ProVideoEditor.instance
|
||||
.renderVideoToFile(media.ffmpegOutputPath.path, task);
|
||||
|
||||
MediaInfo? mediaInfo;
|
||||
try {
|
||||
mediaInfo = await VideoCompress.compressVideo(
|
||||
media.ffmpegOutputPath.path,
|
||||
quality: VideoQuality.Res640x480Quality,
|
||||
includeAudio: true,
|
||||
);
|
||||
Log.info('Video has now size of ${mediaInfo!.filesize} bytes.');
|
||||
} catch (e) {
|
||||
Log.error('during video compression: $e');
|
||||
}
|
||||
if (Platform.isIOS ||
|
||||
media.ffmpegOutputPath.statSync().size >= 10_000_000 ||
|
||||
!kReleaseMode) {
|
||||
String? compressedPath;
|
||||
try {
|
||||
compressedPath = await VideoCompressionChannel.compressVideo(
|
||||
inputPath: media.ffmpegOutputPath.path,
|
||||
outputPath: media.tempPath.path,
|
||||
onProgress: (progress) async {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
media.mediaFile.mediaId,
|
||||
MediaFilesCompanion(
|
||||
preProgressingProcess: Value((progress * 100).toInt()),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('during video compression: $e');
|
||||
}
|
||||
|
||||
if (mediaInfo == null) {
|
||||
Log.error('Could not compress video using original video.');
|
||||
// as a fall back use the non compressed version
|
||||
media.ffmpegOutputPath.copySync(media.tempPath.path);
|
||||
if (compressedPath == null) {
|
||||
Log.error('Could not compress video using original video.');
|
||||
// as a fall back use the non compressed version
|
||||
media.ffmpegOutputPath.copySync(media.tempPath.path);
|
||||
}
|
||||
} 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();
|
||||
|
||||
final sizeFrom = (media.ffmpegOutputPath.statSync().size / 1024 / 1024)
|
||||
.toStringAsFixed(2);
|
||||
final sizeTo =
|
||||
(media.tempPath.statSync().size / 1024 / 1024).toStringAsFixed(2);
|
||||
|
||||
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) {
|
||||
Log.error(e);
|
||||
|
|
|
|||
|
|
@ -166,10 +166,17 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
|
||||
onTap = () => context.push(Routes.settingsSubscription);
|
||||
}
|
||||
if (mediaFile.uploadState == UploadState.preprocessing ||
|
||||
mediaFile.uploadState == UploadState.initialized) {
|
||||
if (mediaFile.uploadState == UploadState.initialized) {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1940,14 +1940,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -106,7 +106,6 @@ dependencies:
|
|||
gal: ^2.3.1
|
||||
google_mlkit_barcode_scanning: ^0.14.1
|
||||
pro_video_editor: ^1.6.1
|
||||
video_compress: ^3.1.4
|
||||
|
||||
dependency_overrides:
|
||||
dots_indicator:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import 'schema_v5.dart' as v5;
|
|||
import 'schema_v6.dart' as v6;
|
||||
import 'schema_v7.dart' as v7;
|
||||
import 'schema_v8.dart' as v8;
|
||||
import 'schema_v9.dart' as v9;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
|
|
@ -33,10 +34,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||
return v7.DatabaseAtV7(db);
|
||||
case 8:
|
||||
return v8.DatabaseAtV8(db);
|
||||
case 9:
|
||||
return v9.DatabaseAtV9(db);
|
||||
default:
|
||||
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];
|
||||
}
|
||||
|
|
|
|||
6403
test/drift/twonly_db/generated/schema_v9.dart
Normal file
6403
test/drift/twonly_db/generated/schema_v9.dart
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue