diff --git a/CHANGELOG.md b/CHANGELOG.md index c1925b0..de321f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.15.0 +- **FEAT**(android, iOS, macOS): Add `NativeLogLevel` parameter to all API methods for controlling native log verbosity per call. Supported levels: `none`, `error`, `warning`, `info`, `debug`, `verbose`. + ## 1.14.4 - **FIX**(android): Fix image layers crash on Android by recycling intermediate thumbnail bitmaps. - **FIX**(android, iOS, macOS): Additional fixes for audio extraction. diff --git a/README.md b/README.md index a66839a..9b4f65a 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ The ProVideoEditor is a Flutter widget designed for video editing within your ap #### ๐Ÿ“ฑ **Runtime Features** - ๐Ÿ“Š **Progress**: Track the progress of one or multiple running tasks. - ๐Ÿงต **Multi-Tasking**: Execute multiple video processing tasks concurrently. +- ๐Ÿ”‡ **Native Log Level**: Control native log verbosity per API call with `NativeLogLevel` (`none`, `error`, `warning`, `info`, `debug`, `verbose`). ### Platform Support @@ -592,6 +593,30 @@ List result = await ProVideoEditor.instance.getKeyFrames( ); ``` +#### Native Log Level Example + +Control native log verbosity per API call on Android, iOS, and macOS. + +```dart +/// Silence all native logs for this call +List thumbnails = await ProVideoEditor.instance.getThumbnails( + ThumbnailConfigs( + video: EditorVideo.asset('assets/my-video.mp4'), + outputSize: const Size(200, 200), + timestamps: const [Duration(seconds: 5)], + ), + nativeLogLevel: NativeLogLevel.none, +); + +/// Show only errors +VideoMetadata metadata = await ProVideoEditor.instance.getMetadata( + video: EditorVideo.asset('assets/my-video.mp4'), + nativeLogLevel: NativeLogLevel.error, +); + +/// Available levels: none, error, warning, info, debug, verbose +``` + ## Sponsors

diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/ProVideoEditorPlugin.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/ProVideoEditorPlugin.kt index e577845..d41af78 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/ProVideoEditorPlugin.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/ProVideoEditorPlugin.kt @@ -2,7 +2,6 @@ package ch.waio.pro_video_editor import android.os.Handler import android.os.Looper -import android.util.Log import ch.waio.pro_video_editor.src.features.audio.ExtractAudio import ch.waio.pro_video_editor.src.features.audio.NoAudioTrackException import ch.waio.pro_video_editor.src.features.audio.models.AudioExtractConfig @@ -12,6 +11,7 @@ import ch.waio.pro_video_editor.src.features.metadata.models.MetadataConfig import ch.waio.pro_video_editor.src.features.render.RenderVideo import ch.waio.pro_video_editor.src.features.render.models.RenderConfig import ch.waio.pro_video_editor.src.features.render.models.RenderTask +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import ch.waio.pro_video_editor.src.features.thumbnail.ThumbnailGenerator import ch.waio.pro_video_editor.src.features.thumbnail.models.ThumbnailConfig import ch.waio.pro_video_editor.src.features.waveform.WaveformGenerator @@ -134,6 +134,8 @@ class ProVideoEditorPlugin : FlutterPlugin, MethodCallHandler { * - cancelTask: Cancels active render or audio extraction task */ override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + applyInlineNativeLogLevel(call) + when (call.method) { "getPlatformVersion" -> handleGetPlatformVersion(result) "getMetadata" -> handleGetMetadata(call, result) @@ -148,6 +150,25 @@ class ProVideoEditorPlugin : FlutterPlugin, MethodCallHandler { } } + /** + * Applies an optional inline native log level from method call arguments. + */ + private fun applyInlineNativeLogLevel(call: MethodCall) { + val level = call.argument("nativeLogLevel") + if (level.isNullOrBlank()) { + return + } + + try { + Log.setMinimumLevel(level) + } catch (e: IllegalArgumentException) { + Log.w( + "ProVideoEditorPlugin", + "Ignoring invalid nativeLogLevel '$level': ${e.message}" + ) + } + } + /** * Returns the Android platform version string. */ diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/audio/ExtractAudio.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/audio/ExtractAudio.kt index c7c8299..d3df4ad 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/audio/ExtractAudio.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/audio/ExtractAudio.kt @@ -7,9 +7,9 @@ import android.media.MediaFormat import android.media.MediaMuxer import android.os.Handler import android.os.Looper -import android.util.Log import ch.waio.pro_video_editor.src.features.audio.models.AudioExtractConfig import ch.waio.pro_video_editor.src.features.audio.models.AudioExtractJobHandle +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import java.io.File import java.nio.ByteBuffer import java.util.concurrent.atomic.AtomicBoolean diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt index 2fd880e..4051de8 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/RenderVideo.kt @@ -4,7 +4,6 @@ import RENDER_TAG import android.content.Context import android.os.Handler import android.os.Looper -import android.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.transformer.Composition import androidx.media3.transformer.DefaultEncoderFactory @@ -12,6 +11,7 @@ import androidx.media3.transformer.ExportException import androidx.media3.transformer.ExportResult import androidx.media3.transformer.ProgressHolder import androidx.media3.transformer.Transformer +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import java.io.File import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyBitrate.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyBitrate.kt index ad05824..f49313c 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyBitrate.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyBitrate.kt @@ -1,9 +1,9 @@ import android.media.MediaCodecInfo import android.media.MediaCodecList -import android.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.transformer.DefaultEncoderFactory import androidx.media3.transformer.VideoEncoderSettings +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log /** * Configures video encoder bitrate settings. diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyBlur.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyBlur.kt index 129aac4..0c5677d 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyBlur.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyBlur.kt @@ -1,7 +1,7 @@ -import android.util.Log import androidx.media3.common.Effect import androidx.media3.common.util.UnstableApi import androidx.media3.effect.GaussianBlur +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log /** * Applies Gaussian blur effect to video. diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyColorMatrix.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyColorMatrix.kt index 687dad8..e6f830c 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyColorMatrix.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyColorMatrix.kt @@ -1,9 +1,9 @@ -import android.util.Log import androidx.media3.common.Effect import androidx.media3.common.util.UnstableApi import androidx.media3.effect.SingleColorLut import androidx.media3.effect.TimestampWrapper import ch.waio.pro_video_editor.src.features.render.models.ColorFilterConfig +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log /** * Applies color matrix transformation using 3D LUT (Look-Up Table). diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyCrop.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyCrop.kt index 5310ff6..239f97b 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyCrop.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyCrop.kt @@ -1,11 +1,11 @@ package ch.waio.pro_video_editor.src.features.render.helpers import RENDER_TAG -import android.util.Log import androidx.media3.common.Effect import androidx.media3.common.util.UnstableApi import androidx.media3.effect.Crop import ch.waio.pro_video_editor.src.features.render.utils.getRotatedVideoDimensions +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import java.io.File /** diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyFlip.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyFlip.kt index 47c758f..e046dda 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyFlip.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyFlip.kt @@ -1,7 +1,7 @@ -import android.util.Log import androidx.media3.common.Effect import androidx.media3.common.util.UnstableApi import androidx.media3.effect.ScaleAndRotateTransformation +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log /** * Applies horizontal and/or vertical flip transformation. diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyImageLayer.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyImageLayer.kt index 3482db6..0e08a58 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyImageLayer.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyImageLayer.kt @@ -3,7 +3,6 @@ package ch.waio.pro_video_editor.src.features.render.helpers import RENDER_TAG import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.util.Log import androidx.media3.common.Effect import androidx.media3.common.util.UnstableApi import androidx.media3.effect.BitmapOverlay @@ -14,6 +13,7 @@ import androidx.core.graphics.scale import androidx.media3.effect.StaticOverlaySettings import androidx.media3.effect.TimestampWrapper import ch.waio.pro_video_editor.src.features.render.models.ImageLayer +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log /** * Applies static image overlay on video. diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyPlaybackSpeed.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyPlaybackSpeed.kt index f004af1..72a7de5 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyPlaybackSpeed.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyPlaybackSpeed.kt @@ -1,9 +1,9 @@ -import android.util.Log import androidx.media3.common.Effect import androidx.media3.common.audio.AudioProcessor import androidx.media3.common.audio.SonicAudioProcessor import androidx.media3.common.util.UnstableApi import androidx.media3.effect.SpeedChangeEffect +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log /** * Applies playback speed modification to both video and audio. diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyRotation.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyRotation.kt index cf495ff..2100d07 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyRotation.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyRotation.kt @@ -1,7 +1,7 @@ -import android.util.Log import androidx.media3.common.Effect import androidx.media3.common.util.UnstableApi import androidx.media3.effect.ScaleAndRotateTransformation +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log /** * Applies video rotation transformation. diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyScale.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyScale.kt index a1e6d24..be1581f 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyScale.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ApplyScale.kt @@ -1,7 +1,7 @@ -import android.util.Log import androidx.media3.common.Effect import androidx.media3.common.util.UnstableApi import androidx.media3.effect.ScaleAndRotateTransformation +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log /** * Applies scale transformation to video dimensions. diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/AudioSequenceBuilder.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/AudioSequenceBuilder.kt index f67281a..8a72386 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/AudioSequenceBuilder.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/AudioSequenceBuilder.kt @@ -2,7 +2,6 @@ package ch.waio.pro_video_editor.src.features.render.helpers import RENDER_TAG import android.net.Uri -import android.util.Log import androidx.media3.common.MediaItem import androidx.media3.common.audio.AudioProcessor import androidx.media3.common.audio.ChannelMixingAudioProcessor @@ -11,6 +10,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.transformer.EditedMediaItem import androidx.media3.transformer.EditedMediaItemSequence import androidx.media3.transformer.Effects +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import java.io.File /** diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/CompositionBuilder.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/CompositionBuilder.kt index 9965a68..a83ae3b 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/CompositionBuilder.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/CompositionBuilder.kt @@ -2,7 +2,6 @@ package ch.waio.pro_video_editor.src.features.render.helpers import RENDER_TAG import android.content.Context -import android.util.Log import androidx.media3.common.Effect import androidx.media3.common.audio.AudioProcessor import androidx.media3.common.util.UnstableApi @@ -10,6 +9,7 @@ import androidx.media3.transformer.Composition import androidx.media3.transformer.EditedMediaItemSequence import ch.waio.pro_video_editor.src.features.render.models.AudioTrackConfig import ch.waio.pro_video_editor.src.features.render.models.RenderConfig +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log /** * Main builder class for creating Media3 Compositions from render configurations. diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ConfigurableInAppMp4Muxer.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ConfigurableInAppMp4Muxer.kt index 55268d0..0abc92e 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ConfigurableInAppMp4Muxer.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/ConfigurableInAppMp4Muxer.kt @@ -1,6 +1,5 @@ package ch.waio.pro_video_editor.src.features.render.helpers -import android.util.Log import androidx.media3.common.C import androidx.media3.common.Format import androidx.media3.common.Metadata @@ -13,6 +12,7 @@ import androidx.media3.muxer.Muxer import androidx.media3.muxer.MuxerException import androidx.media3.muxer.MuxerUtil import androidx.media3.muxer.SeekableMuxerOutput +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import com.google.common.collect.ImmutableList import java.io.FileNotFoundException import java.io.FileOutputStream diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/MediaInfoExtractor.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/MediaInfoExtractor.kt index d38f6d7..9c88589 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/MediaInfoExtractor.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/MediaInfoExtractor.kt @@ -3,8 +3,8 @@ package ch.waio.pro_video_editor.src.features.render.helpers import RENDER_TAG import android.media.MediaExtractor import android.media.MediaFormat -import android.util.Log import androidx.media3.common.util.UnstableApi +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log /** * Utility class for extracting media information from video and audio files. diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt index 042c45d..8193e0c 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoSequenceBuilder.kt @@ -2,7 +2,6 @@ package ch.waio.pro_video_editor.src.features.render.helpers import RENDER_TAG import android.net.Uri -import android.util.Log import applyScale import androidx.media3.common.C import androidx.media3.common.Effect @@ -17,6 +16,7 @@ import androidx.media3.transformer.Effects import ch.waio.pro_video_editor.src.features.render.models.LayerAnimationConfig import ch.waio.pro_video_editor.src.features.render.models.VideoClip import ch.waio.pro_video_editor.src.features.render.utils.getRotatedVideoDimensions +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import java.io.File /** diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoTranscoder.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoTranscoder.kt index 2199320..84d9a06 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoTranscoder.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VideoTranscoder.kt @@ -4,7 +4,6 @@ import RENDER_TAG import android.content.Context import android.os.Handler import android.os.Looper -import android.util.Log import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi @@ -15,6 +14,7 @@ import androidx.media3.transformer.EditedMediaItemSequence import androidx.media3.transformer.ExportException import androidx.media3.transformer.ExportResult import androidx.media3.transformer.Transformer +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import java.io.File import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicReference diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VolumeAudioProcessor.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VolumeAudioProcessor.kt index 14f53a3..fb7f785 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VolumeAudioProcessor.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VolumeAudioProcessor.kt @@ -1,9 +1,9 @@ package ch.waio.pro_video_editor.src.features.render.helpers import RENDER_TAG -import android.util.Log import androidx.media3.common.audio.BaseAudioProcessor import androidx.media3.common.util.UnstableApi +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import java.nio.ByteBuffer /** diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VolumeControlAudioMixer.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VolumeControlAudioMixer.kt index b5c3c52..11af2ee 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VolumeControlAudioMixer.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/helpers/VolumeControlAudioMixer.kt @@ -1,11 +1,11 @@ package ch.waio.pro_video_editor.src.features.render.helpers import RENDER_TAG -import android.util.Log import androidx.media3.common.audio.AudioProcessor import androidx.media3.common.util.UnstableApi import androidx.media3.transformer.AudioMixer import androidx.media3.transformer.DefaultAudioMixer +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import java.nio.ByteBuffer /** diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt index 25a4ded..bb71bc3 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/render/models/RenderConfig.kt @@ -1,7 +1,7 @@ package ch.waio.pro_video_editor.src.features.render.models import PACKAGE_TAG -import android.util.Log +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import io.flutter.plugin.common.MethodCall /** diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/thumbnail/ThumbnailGenerator.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/thumbnail/ThumbnailGenerator.kt index 8899394..8618269 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/thumbnail/ThumbnailGenerator.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/thumbnail/ThumbnailGenerator.kt @@ -6,8 +6,8 @@ import android.graphics.Bitmap import android.media.MediaExtractor import android.media.MediaFormat import android.media.MediaMetadataRetriever -import android.util.Log import ch.waio.pro_video_editor.src.features.thumbnail.models.ThumbnailConfig +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/waveform/WaveformGenerator.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/waveform/WaveformGenerator.kt index df96def..0674fdd 100644 --- a/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/waveform/WaveformGenerator.kt +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/features/waveform/WaveformGenerator.kt @@ -6,10 +6,10 @@ import android.media.MediaExtractor import android.media.MediaFormat import android.os.Handler import android.os.Looper -import android.util.Log import ch.waio.pro_video_editor.src.features.audio.NoAudioTrackException import ch.waio.pro_video_editor.src.features.waveform.models.WaveformConfig import ch.waio.pro_video_editor.src.features.waveform.models.WaveformJobHandle +import ch.waio.pro_video_editor.src.shared.logging.PluginLog as Log import java.nio.ByteBuffer import java.nio.ByteOrder import java.util.concurrent.atomic.AtomicBoolean diff --git a/android/src/main/kotlin/ch/waio/pro_video_editor/src/shared/logging/PluginLog.kt b/android/src/main/kotlin/ch/waio/pro_video_editor/src/shared/logging/PluginLog.kt new file mode 100644 index 0000000..3ff64e0 --- /dev/null +++ b/android/src/main/kotlin/ch/waio/pro_video_editor/src/shared/logging/PluginLog.kt @@ -0,0 +1,68 @@ +package ch.waio.pro_video_editor.src.shared.logging + +import android.util.Log as AndroidLog + +object PluginLog { + private const val LEVEL_NONE = Int.MAX_VALUE + + @Volatile + private var minimumPriority = defaultPriority() + + fun setMinimumLevel(level: String) { + minimumPriority = parsePriority(level) + } + + private fun defaultPriority(): Int { + val isDebugBuild = runCatching { + Class.forName("ch.waio.pro_video_editor.BuildConfig") + .getField("DEBUG") + .getBoolean(null) + }.getOrDefault(true) + + return if (isDebugBuild) AndroidLog.DEBUG else AndroidLog.WARN + } + + private fun parsePriority(level: String): Int { + return when (level.lowercase()) { + "none" -> LEVEL_NONE + "error" -> AndroidLog.ERROR + "warn", "warning" -> AndroidLog.WARN + "info" -> AndroidLog.INFO + "debug" -> AndroidLog.DEBUG + "verbose" -> AndroidLog.VERBOSE + else -> throw IllegalArgumentException("Unsupported Android log level: $level") + } + } + + private fun shouldLog(priority: Int): Boolean { + return priority >= minimumPriority + } + + fun v(tag: String, message: String): Int { + return if (shouldLog(AndroidLog.VERBOSE)) AndroidLog.v(tag, message) else 0 + } + + fun d(tag: String, message: String): Int { + return if (shouldLog(AndroidLog.DEBUG)) AndroidLog.d(tag, message) else 0 + } + + fun i(tag: String, message: String): Int { + return if (shouldLog(AndroidLog.INFO)) AndroidLog.i(tag, message) else 0 + } + + fun w(tag: String, message: String): Int { + return if (shouldLog(AndroidLog.WARN)) AndroidLog.w(tag, message) else 0 + } + + fun w(tag: String, message: String, throwable: Throwable): Int { + return if (shouldLog(AndroidLog.WARN)) AndroidLog.w(tag, message, throwable) else 0 + } + + fun e(tag: String, message: String): Int { + return if (shouldLog(AndroidLog.ERROR)) AndroidLog.e(tag, message) else 0 + } + + fun e(tag: String, message: String, throwable: Throwable): Int { + return if (shouldLog(AndroidLog.ERROR)) AndroidLog.e(tag, message, throwable) else 0 + } +} \ No newline at end of file diff --git a/ios/Classes/ProVideoEditorPlugin.swift b/ios/Classes/ProVideoEditorPlugin.swift index 8536667..345dcd8 100644 --- a/ios/Classes/ProVideoEditorPlugin.swift +++ b/ios/Classes/ProVideoEditorPlugin.swift @@ -50,6 +50,8 @@ public class ProVideoEditorPlugin: NSObject, FlutterPlugin { /// - extractAudio: Extracts audio from video /// - cancelTask: Cancels active render or audio extraction task public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + applyInlineNativeLogLevel(call: call) + switch call.method { case "getPlatformVersion": handleGetPlatformVersion(result: result) @@ -83,6 +85,21 @@ public class ProVideoEditorPlugin: NSObject, FlutterPlugin { } } + private func applyInlineNativeLogLevel(call: FlutterMethodCall) { + guard let args = call.arguments as? [String: Any], + let level = args["nativeLogLevel"] as? String, + !level.isEmpty + else { + return + } + + do { + try PluginLog.setMinimumLevel(level) + } catch { + PluginLog.print("โš ๏ธ Ignoring invalid nativeLogLevel '\(level)': \(error.localizedDescription)") + } + } + // MARK: - Handler Methods /// Returns the iOS platform version string. diff --git a/ios/Classes/src/features/render/RenderVideo.swift b/ios/Classes/src/features/render/RenderVideo.swift index b4ec530..5a78609 100644 --- a/ios/Classes/src/features/render/RenderVideo.swift +++ b/ios/Classes/src/features/render/RenderVideo.swift @@ -55,7 +55,7 @@ class RenderVideo { // HEVC 10-bit HDR videos cause issues with AVFoundation's compositor // They must be pre-transcoded to H.264 8-bit SDR for ANY effect processing - print("๐Ÿ” Checking for HEVC 10-bit videos that need transcoding...") + PluginLog.print("๐Ÿ” Checking for HEVC 10-bit videos that need transcoding...") // Pre-transcode HEVC 10-bit HDR videos to H.264 8-bit SDR let inputPaths = config.videoClips.map { $0.inputPath } @@ -65,7 +65,7 @@ class RenderVideo { transcodedFiles = transcodeMap.values.filter { $0.contains("transcoded_") } if !transcodedFiles.isEmpty { - print("โœ… Pre-transcoded \(transcodedFiles.count) HEVC 10-bit videos to H.264") + PluginLog.print("โœ… Pre-transcoded \(transcodedFiles.count) HEVC 10-bit videos to H.264") // Update config with transcoded paths let updatedClips = config.videoClips.map { clip -> VideoClip in @@ -108,10 +108,10 @@ class RenderVideo { let requestedFormat = workingConfig.outputFormat.lowercased() if pathExtension != requestedFormat { - print( + PluginLog.print( "โš ๏ธ WARNING: Output path extension '.\(pathExtension)' doesn't match requested format '.\(requestedFormat)'" ) - print("โš ๏ธ Correcting file extension to match format...") + PluginLog.print("โš ๏ธ Correcting file extension to match format...") // Replace extension with correct format let pathWithoutExtension = url.deletingPathExtension() @@ -123,16 +123,16 @@ class RenderVideo { outputURL = temporaryURL(for: workingConfig.outputFormat) } - print("") - print("๐ŸŽฌ ===== RENDER CONFIG =====") - print(" Video clips: \(workingConfig.videoClips.count)") - print(" ๐Ÿ“ Output format: \(workingConfig.outputFormat)") - print(" ๐Ÿ“น Output path: \(outputURL.path)") - print(" ๐Ÿ”Š Enable Audio: \(workingConfig.enableAudio)") - print(" ๐ŸŽต Audio tracks: \(workingConfig.audioTracks.count)") - print(" ๐ŸŽจ Color filters: \(workingConfig.colorFilters.count)") - print("===========================") - print("") + PluginLog.print("") + PluginLog.print("๐ŸŽฌ ===== RENDER CONFIG =====") + PluginLog.print(" Video clips: \(workingConfig.videoClips.count)") + PluginLog.print(" ๐Ÿ“ Output format: \(workingConfig.outputFormat)") + PluginLog.print(" ๐Ÿ“น Output path: \(outputURL.path)") + PluginLog.print(" ๐Ÿ”Š Enable Audio: \(workingConfig.enableAudio)") + PluginLog.print(" ๐ŸŽต Audio tracks: \(workingConfig.audioTracks.count)") + PluginLog.print(" ๐ŸŽจ Color filters: \(workingConfig.colorFilters.count)") + PluginLog.print("===========================") + PluginLog.print("") // Create configuration for video effects var effectsConfig = VideoCompositorConfig() @@ -335,10 +335,10 @@ class RenderVideo { } let fileType = mapFormatToMimeType(format: outputFormat) - print("๐Ÿ“น Export session setup:") - print(" - Requested format: \(outputFormat)") - print(" - AVFileType: \(fileType.rawValue)") - print(" - Output URL: \(outputURL.path)") + PluginLog.print("๐Ÿ“น Export session setup:") + PluginLog.print(" - Requested format: \(outputFormat)") + PluginLog.print(" - AVFileType: \(fileType.rawValue)") + PluginLog.print(" - Output URL: \(outputURL.path)") export.outputURL = outputURL export.outputFileType = fileType @@ -363,7 +363,7 @@ class RenderVideo { if CMTimeGetSeconds(clampedDuration) > 0 { export.timeRange = CMTimeRange(start: startTime, duration: clampedDuration) - print( + PluginLog.print( " - TimeRange applied: \(String(format: "%.2f", CMTimeGetSeconds(startTime)))s - \(String(format: "%.2f", CMTimeGetSeconds(CMTimeAdd(startTime, clampedDuration))))s" ) } @@ -376,15 +376,15 @@ class RenderVideo { // Apply audio mix if available if let audioMix = audioMix, hasAudioTracks { export.audioMix = audioMix - print("๐Ÿ”Š Audio mix applied to export session") + PluginLog.print("๐Ÿ”Š Audio mix applied to export session") } else if !hasAudioTracks { - print("โ„น๏ธ No audio tracks in composition - exporting video only") + PluginLog.print("โ„น๏ธ No audio tracks in composition - exporting video only") } // Apply fast start optimization (moves moov atom to beginning for streaming) export.shouldOptimizeForNetworkUse = shouldOptimizeForNetworkUse if shouldOptimizeForNetworkUse { - print("๐Ÿš€ Fast start enabled - optimizing for network streaming") + PluginLog.print("๐Ÿš€ Fast start enabled - optimizing for network streaming") } return export diff --git a/ios/Classes/src/features/render/helpers/ApplyBitrate.swift b/ios/Classes/src/features/render/helpers/ApplyBitrate.swift index 12ffdf2..9b84b4a 100644 --- a/ios/Classes/src/features/render/helpers/ApplyBitrate.swift +++ b/ios/Classes/src/features/render/helpers/ApplyBitrate.swift @@ -25,10 +25,10 @@ import AVFoundation /// - <1 Mbps: Low quality public func applyBitrate(requestedBitrate: Int?, presetHint: String? = nil) -> String { if let bitrate = requestedBitrate { - print( + PluginLog.print( "[\(Tags.render)] ๐Ÿ“Š Requested bitrate: \(bitrate) bps (\(String(format: "%.1f", Double(bitrate) / 1_000_000)) Mbps)" ) - print( + PluginLog.print( "[\(Tags.render)] โš ๏ธ AVAssetExportSession does not support custom bitrate directly - using closest preset" ) diff --git a/ios/Classes/src/features/render/helpers/ApplyBlur.swift b/ios/Classes/src/features/render/helpers/ApplyBlur.swift index 4b8ad30..2e878f4 100644 --- a/ios/Classes/src/features/render/helpers/ApplyBlur.swift +++ b/ios/Classes/src/features/render/helpers/ApplyBlur.swift @@ -18,5 +18,5 @@ func applyBlur( if sigma == nil || sigma == 0 { return } - print("[\(Tags.render)] ๐ŸŒซ๏ธ Applying Gaussian blur: sigma=\(String(format: "%.1f", sigma!))") + PluginLog.print("[\(Tags.render)] ๐ŸŒซ๏ธ Applying Gaussian blur: sigma=\(String(format: "%.1f", sigma!))") } diff --git a/ios/Classes/src/features/render/helpers/ApplyColorMatrix.swift b/ios/Classes/src/features/render/helpers/ApplyColorMatrix.swift index c7f0967..5e14478 100644 --- a/ios/Classes/src/features/render/helpers/ApplyColorMatrix.swift +++ b/ios/Classes/src/features/render/helpers/ApplyColorMatrix.swift @@ -25,7 +25,7 @@ func applyColorMatrix( let globalCount = filters.filter { $0.startUs == -1 && $0.endUs == -1 }.count let timedCount = filters.count - globalCount - print( + PluginLog.print( "[\(Tags.render)] ๐ŸŽจ Applying color grading: \(filters.count) filter(s) (\(globalCount) global, \(timedCount) timed)" ) } @@ -34,7 +34,7 @@ func applyColorMatrix( func multiplyColorMatrices(_ m1: [Double], _ m2: [Double]) -> [Double] { guard m1.count == 20, m2.count == 20 else { - print("Invalid matrix dimensions for multiplication") + PluginLog.print("Invalid matrix dimensions for multiplication") return m1 } diff --git a/ios/Classes/src/features/render/helpers/ApplyCrop.swift b/ios/Classes/src/features/render/helpers/ApplyCrop.swift index 1984dd6..fa39250 100644 --- a/ios/Classes/src/features/render/helpers/ApplyCrop.swift +++ b/ios/Classes/src/features/render/helpers/ApplyCrop.swift @@ -37,7 +37,7 @@ func applyCrop( config.cropHeight = height if cropX != 0 || cropY != 0 || cropWidth != nil || cropHeight != nil { - print( + PluginLog.print( "[\(Tags.render)] โœ‚๏ธ Applying crop: x=\(Int(x)), y=\(Int(y)), width=\(Int(width)), height=\(Int(height))" ) } diff --git a/ios/Classes/src/features/render/helpers/ApplyFlip.swift b/ios/Classes/src/features/render/helpers/ApplyFlip.swift index 0eecdd5..9fe2cf0 100644 --- a/ios/Classes/src/features/render/helpers/ApplyFlip.swift +++ b/ios/Classes/src/features/render/helpers/ApplyFlip.swift @@ -20,5 +20,5 @@ func applyFlip( if !flipX && !flipY { return } let flipType = flipX && flipY ? "both axes" : flipX ? "horizontal" : "vertical" - print("[\(Tags.render)] ๐Ÿ”„ Applying flip: \(flipType)") + PluginLog.print("[\(Tags.render)] ๐Ÿ”„ Applying flip: \(flipType)") } diff --git a/ios/Classes/src/features/render/helpers/ApplyImageLayer.swift b/ios/Classes/src/features/render/helpers/ApplyImageLayer.swift index b3f3963..62558ab 100644 --- a/ios/Classes/src/features/render/helpers/ApplyImageLayer.swift +++ b/ios/Classes/src/features/render/helpers/ApplyImageLayer.swift @@ -25,6 +25,6 @@ func applyImageLayer( config.imageBytesWithCropping = withCropping if !imageLayers.isEmpty { - print("[\(Tags.render)] ๐Ÿ–ผ๏ธ Applying \(imageLayers.count) image layer(s) with timing") + PluginLog.print("[\(Tags.render)] ๐Ÿ–ผ๏ธ Applying \(imageLayers.count) image layer(s) with timing") } } diff --git a/ios/Classes/src/features/render/helpers/ApplyPlaybackSpeed.swift b/ios/Classes/src/features/render/helpers/ApplyPlaybackSpeed.swift index b8226b5..4099370 100644 --- a/ios/Classes/src/features/render/helpers/ApplyPlaybackSpeed.swift +++ b/ios/Classes/src/features/render/helpers/ApplyPlaybackSpeed.swift @@ -25,7 +25,7 @@ public func applyPlaybackSpeed( guard let speed = speed, speed > 0, speed != 1 else { return instructions } let speedType = speed < 1 ? "slow motion" : "fast forward" - print("[\(Tags.render)] โšก Applying playback speed: \(String(format: "%.2f", speed))x (\(speedType))") + PluginLog.print("[\(Tags.render)] โšก Applying playback speed: \(String(format: "%.2f", speed))x (\(speedType))") let multiplier = 1.0 / Double(speed) diff --git a/ios/Classes/src/features/render/helpers/ApplyRotation.swift b/ios/Classes/src/features/render/helpers/ApplyRotation.swift index 6e0de12..04a8a27 100644 --- a/ios/Classes/src/features/render/helpers/ApplyRotation.swift +++ b/ios/Classes/src/features/render/helpers/ApplyRotation.swift @@ -28,5 +28,5 @@ func applyRotation( config.rotateTurns = turns if turns == 0 { return } - print("[\(Tags.render)] ๐Ÿ”„ Applying rotation: \(degrees)ยฐ (\(turns) ร— 90ยฐ)") + PluginLog.print("[\(Tags.render)] ๐Ÿ”„ Applying rotation: \(degrees)ยฐ (\(turns) ร— 90ยฐ)") } diff --git a/ios/Classes/src/features/render/helpers/ApplyScale.swift b/ios/Classes/src/features/render/helpers/ApplyScale.swift index 38ec20e..42ab7a4 100644 --- a/ios/Classes/src/features/render/helpers/ApplyScale.swift +++ b/ios/Classes/src/features/render/helpers/ApplyScale.swift @@ -25,6 +25,6 @@ func applyScale( if x != 1.0 || y != 1.0 { let percentX = Int(x * 100) let percentY = Int(y * 100) - print("[\(Tags.render)] ๐Ÿ“ Applying scale: X=\(percentX)%, Y=\(percentY)%") + PluginLog.print("[\(Tags.render)] ๐Ÿ“ Applying scale: X=\(percentX)%, Y=\(percentY)%") } } diff --git a/ios/Classes/src/features/render/helpers/AudioSequenceBuilder.swift b/ios/Classes/src/features/render/helpers/AudioSequenceBuilder.swift index e171e3a..71431a9 100644 --- a/ios/Classes/src/features/render/helpers/AudioSequenceBuilder.swift +++ b/ios/Classes/src/features/render/helpers/AudioSequenceBuilder.swift @@ -101,7 +101,7 @@ internal class AudioSequenceBuilder { func build(in composition: AVMutableComposition) async throws -> AVMutableCompositionTrack? { let audioURL = URL(fileURLWithPath: audioPath) guard FileManager.default.fileExists(atPath: audioURL.path) else { - print("โš ๏ธ Custom audio file does not exist: \(audioPath)") + PluginLog.print("โš ๏ธ Custom audio file does not exist: \(audioPath)") return nil } @@ -113,7 +113,7 @@ internal class AudioSequenceBuilder { preferredTrackID: kCMPersistentTrackID_Invalid ) else { - print("โš ๏ธ Failed to add custom audio track") + PluginLog.print("โš ๏ธ Failed to add custom audio track") return nil } @@ -129,7 +129,7 @@ internal class AudioSequenceBuilder { let effectiveAudioEnd = audioEndTime ?? audioDuration let effectiveAudioDuration = CMTimeSubtract(effectiveAudioEnd, audioStartTime) if CMTimeCompare(effectiveAudioDuration, .zero) <= 0 { - print( + PluginLog.print( "โš ๏ธ Audio start/end time range is invalid (start: \(audioStartTime.seconds)s, end: \(effectiveAudioEnd.seconds)s)" ) return nil @@ -141,18 +141,18 @@ internal class AudioSequenceBuilder { let effectivePlayDuration = CMTimeMinimum(playDuration, remainingCompositionTime) if CMTimeCompare(effectivePlayDuration, .zero) <= 0 { - print("โš ๏ธ No time remaining in composition for audio track") + PluginLog.print("โš ๏ธ No time remaining in composition for audio track") return nil } if CMTimeCompare(audioStartTime, .zero) > 0 { - print("๐ŸŽต Custom audio start offset: \(audioStartTime.seconds)s") + PluginLog.print("๐ŸŽต Custom audio start offset: \(audioStartTime.seconds)s") } if audioEndTime != nil { - print("๐ŸŽต Custom audio end offset: \(effectiveAudioEnd.seconds)s") + PluginLog.print("๐ŸŽต Custom audio end offset: \(effectiveAudioEnd.seconds)s") } if CMTimeCompare(compositionInsertTime, .zero) > 0 { - print( + PluginLog.print( "๐ŸŽต Audio placed at composition time: \(compositionInsertTime.seconds)s" ) } @@ -163,7 +163,7 @@ internal class AudioSequenceBuilder { let timeRange = CMTimeRange(start: audioStartTime, duration: effectivePlayDuration) try compositionAudioTrack.insertTimeRange( timeRange, of: audioTrack, at: compositionInsertTime) - print("โœ‚๏ธ Custom audio trimmed to \(effectivePlayDuration.seconds)s") + PluginLog.print("โœ‚๏ธ Custom audio trimmed to \(effectivePlayDuration.seconds)s") } else if loopAudio { // Loop audio to match play duration var currentTime = compositionInsertTime @@ -188,7 +188,7 @@ internal class AudioSequenceBuilder { isFirstLoop = false } - print( + PluginLog.print( "๐Ÿ”„ Custom audio looped \(loopCount) times to match \(effectivePlayDuration.seconds)s duration" ) } else { @@ -197,14 +197,14 @@ internal class AudioSequenceBuilder { let timeRange = CMTimeRange(start: audioStartTime, duration: insertDuration) try compositionAudioTrack.insertTimeRange( timeRange, of: audioTrack, at: compositionInsertTime) - print( + PluginLog.print( "โ–ถ๏ธ Custom audio plays once (\(insertDuration.seconds)s, no loop)" + (CMTimeCompare(audioStartTime, .zero) > 0 ? " starting at \(audioStartTime.seconds)s" : "")) } if volume != 1.0 { - print("๐Ÿ”Š Custom audio volume: \(volume)") + PluginLog.print("๐Ÿ”Š Custom audio volume: \(volume)") } return compositionAudioTrack @@ -218,7 +218,7 @@ internal class AudioSequenceBuilder { let customSampleRate = await MediaInfoExtractor.getAudioSampleRate(audioPath) guard customSampleRate > 0 else { - print("โš ๏ธ Could not detect custom audio sample rate") + PluginLog.print("โš ๏ธ Could not detect custom audio sample rate") return true // Assume compatible if we can't detect } @@ -226,14 +226,14 @@ internal class AudioSequenceBuilder { if let videoSampleRate = await getVideoAudioSampleRate(clip.inputPath), videoSampleRate > 0 && videoSampleRate != customSampleRate { - print( + PluginLog.print( "โŒ Sample rate mismatch: custom audio (\(customSampleRate) Hz) vs video (\(videoSampleRate) Hz)" ) return false } } - print("โœ… Sample rates are compatible") + PluginLog.print("โœ… Sample rates are compatible") return true } diff --git a/ios/Classes/src/features/render/helpers/CompositionBuilder.swift b/ios/Classes/src/features/render/helpers/CompositionBuilder.swift index 91bbd7f..0590ec2 100644 --- a/ios/Classes/src/features/render/helpers/CompositionBuilder.swift +++ b/ios/Classes/src/features/render/helpers/CompositionBuilder.swift @@ -56,8 +56,8 @@ internal class CompositionBuilder { ) } - print("๐ŸŽฌ Creating composition with \(videoClips.count) video clips") - print("๐Ÿ”Š Audio enabled: \(enableAudio)") + PluginLog.print("๐ŸŽฌ Creating composition with \(videoClips.count) video clips") + PluginLog.print("๐Ÿ”Š Audio enabled: \(enableAudio)") let composition = AVMutableComposition() @@ -70,7 +70,7 @@ internal class CompositionBuilder { // Add custom audio tracks var customAudioTracks: [(track: AVMutableCompositionTrack, config: AudioTrackConfig)] = [] for trackConfig in audioTracks { - print("๐ŸŽต Adding audio track: \(trackConfig.path)") + PluginLog.print("๐ŸŽต Adding audio track: \(trackConfig.path)") let audioBuilder = AudioSequenceBuilder( audioPath: trackConfig.path, targetDuration: videoResult.totalDuration @@ -111,18 +111,18 @@ internal class CompositionBuilder { // This fixes issues on older iOS versions (e.g., iPhone 7, iOS 15) var instructions: [AVVideoCompositionInstructionProtocol] = [] - print("") - print("๐ŸŽจ ===== CREATING VIDEO INSTRUCTIONS =====") - print(" Total clips to process: \(videoResult.clipInstructions.count)") - print( + PluginLog.print("") + PluginLog.print("๐ŸŽจ ===== CREATING VIDEO INSTRUCTIONS =====") + PluginLog.print(" Total clips to process: \(videoResult.clipInstructions.count)") + PluginLog.print( " Target render size: \(videoResult.renderSize.width) x \(videoResult.renderSize.height)" ) - print("==========================================") - print("") + PluginLog.print("==========================================") + PluginLog.print("") for (index, clipInstruction) in videoResult.clipInstructions.enumerated() { - print("๐ŸŽฌ Processing instruction for clip \(index)") - print( + PluginLog.print("๐ŸŽฌ Processing instruction for clip \(index)") + PluginLog.print( " Time range: \(String(format: "%.2f", clipInstruction.timeRange.start.seconds))s - \(String(format: "%.2f", (clipInstruction.timeRange.start + clipInstruction.timeRange.duration).seconds))s" ) @@ -157,10 +157,10 @@ internal class CompositionBuilder { backgroundColor: CGColor(red: 0, green: 0, blue: 0, alpha: 1) ) - print( + PluginLog.print( " โš™๏ธ Layer instruction configured with transform (trackID: \(videoResult.videoTrack.trackID))" ) - print("") + PluginLog.print("") instructions.append(instruction) } @@ -171,7 +171,7 @@ internal class CompositionBuilder { renderSize: compositionRenderSize ) - print("โœ… Composition created successfully with \(videoClips.count) clips") + PluginLog.print("โœ… Composition created successfully with \(videoClips.count) clips") // Return the track ID for fallback on older iOS versions let sourceTrackID = videoResult.videoTrack.trackID @@ -204,7 +204,7 @@ internal class CompositionBuilder { } audioMixInputParameters.append(inputParameters) - print("๐Ÿ”Š Applied per-clip volume to original audio track") + PluginLog.print("๐Ÿ”Š Applied per-clip volume to original audio track") } // Apply volume to custom audio tracks @@ -212,7 +212,7 @@ internal class CompositionBuilder { let inputParameters = AVMutableAudioMixInputParameters(track: track) inputParameters.setVolume(config.volume, at: .zero) audioMixInputParameters.append(inputParameters) - print("๐Ÿ”Š Applied volume \(config.volume) to custom audio track: \(config.path)") + PluginLog.print("๐Ÿ”Š Applied volume \(config.volume) to custom audio track: \(config.path)") } let audioMix = AVMutableAudioMix() @@ -239,10 +239,10 @@ internal class CompositionBuilder { let videoWidth = abs(displaySize.width) let videoHeight = abs(displaySize.height) - print(" ๐Ÿ“ Transform calculation:") - print(" Natural size: \(naturalSize.width) x \(naturalSize.height)") - print(" Display size (after rotation): \(videoWidth) x \(videoHeight)") - print(" Target render size: \(renderSize.width) x \(renderSize.height)") + PluginLog.print(" ๐Ÿ“ Transform calculation:") + PluginLog.print(" Natural size: \(naturalSize.width) x \(naturalSize.height)") + PluginLog.print(" Display size (after rotation): \(videoWidth) x \(videoHeight)") + PluginLog.print(" Target render size: \(renderSize.width) x \(renderSize.height)") // Calculate scale to fill the render size (we want videos to be the same size) let scaleX = renderSize.width / videoWidth @@ -253,21 +253,21 @@ internal class CompositionBuilder { let scalePercentage = scale * 100 if willBeScaled { - print( + PluginLog.print( " ๐Ÿ” SCALING: \(String(format: "%.1f%%", scalePercentage)) (factor: \(String(format: "%.3f", scale)))" ) - print( + PluginLog.print( " Scale X: \(String(format: "%.3f", scaleX)) | Scale Y: \(String(format: "%.3f", scaleY))" ) } else { - print(" โœ“ No scaling needed (video already fits render size)") + PluginLog.print(" โœ“ No scaling needed (video already fits render size)") } // Calculate the scaled video dimensions let scaledWidth = videoWidth * scale let scaledHeight = videoHeight * scale - print( + PluginLog.print( " Final video size: \(String(format: "%.1f", scaledWidth)) x \(String(format: "%.1f", scaledHeight))" ) @@ -281,7 +281,7 @@ internal class CompositionBuilder { let angle = atan2(preferredTransform.b, preferredTransform.a) let degrees = angle * 180 / .pi - print(" Rotation: \(String(format: "%.1f", degrees))ยฐ") + PluginLog.print(" Rotation: \(String(format: "%.1f", degrees))ยฐ") // 2. Scale the video to fit the render size transform = transform.scaledBy(x: scale, y: scale) @@ -298,20 +298,20 @@ internal class CompositionBuilder { finalTranslateX = translateY finalTranslateY = translateX transform = transform.translatedBy(x: finalTranslateX, y: finalTranslateY) - print( + PluginLog.print( " Translation (rotated coords): x=\(String(format: "%.1f", finalTranslateX)), y=\(String(format: "%.1f", finalTranslateY))" ) } else { finalTranslateX = translateX finalTranslateY = translateY transform = transform.translatedBy(x: finalTranslateX, y: finalTranslateY) - print( + PluginLog.print( " Translation: x=\(String(format: "%.1f", finalTranslateX)), y=\(String(format: "%.1f", finalTranslateY))" ) } - print(" โœ… Transform applied for clip \(clipIndex)") - print("") + PluginLog.print(" โœ… Transform applied for clip \(clipIndex)") + PluginLog.print("") return transform } diff --git a/ios/Classes/src/features/render/helpers/MediaInfoExtractor.swift b/ios/Classes/src/features/render/helpers/MediaInfoExtractor.swift index a31db11..edcf536 100644 --- a/ios/Classes/src/features/render/helpers/MediaInfoExtractor.swift +++ b/ios/Classes/src/features/render/helpers/MediaInfoExtractor.swift @@ -16,7 +16,7 @@ internal class MediaInfoExtractor { static func getVideoDuration(_ videoPath: String) async -> Int64 { let url = URL(fileURLWithPath: videoPath) guard FileManager.default.fileExists(atPath: url.path) else { - print("โŒ Video file does not exist: \(videoPath)") + PluginLog.print("โŒ Video file does not exist: \(videoPath)") return 0 } @@ -36,7 +36,7 @@ internal class MediaInfoExtractor { return Int64(duration.seconds * 1_000_000) } catch { - print("โŒ Failed to get video duration for \(videoPath): \(error.localizedDescription)") + PluginLog.print("โŒ Failed to get video duration for \(videoPath): \(error.localizedDescription)") return 0 } } @@ -48,7 +48,7 @@ internal class MediaInfoExtractor { static func getAudioDuration(_ audioPath: String) async -> Int64 { let url = URL(fileURLWithPath: audioPath) guard FileManager.default.fileExists(atPath: url.path) else { - print("โŒ Audio file does not exist: \(audioPath)") + PluginLog.print("โŒ Audio file does not exist: \(audioPath)") return 0 } @@ -67,10 +67,10 @@ internal class MediaInfoExtractor { } let durationUs = Int64(duration.seconds * 1_000_000) - print("๐Ÿ” Audio duration: \(durationUs / 1000) ms") + PluginLog.print("๐Ÿ” Audio duration: \(durationUs / 1000) ms") return durationUs } catch { - print("โŒ Failed to get audio duration: \(error.localizedDescription)") + PluginLog.print("โŒ Failed to get audio duration: \(error.localizedDescription)") return 0 } } @@ -117,7 +117,7 @@ internal class MediaInfoExtractor { return nil } catch { - print("โŒ Failed to get audio channel count: \(error.localizedDescription)") + PluginLog.print("โŒ Failed to get audio channel count: \(error.localizedDescription)") return nil } } @@ -159,14 +159,14 @@ internal class MediaInfoExtractor { let formatDesc = description as! CMFormatDescription if let basicDesc = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc) { let sampleRate = Int(basicDesc.pointee.mSampleRate) - print("๐Ÿ” Audio sample rate: \(sampleRate) Hz") + PluginLog.print("๐Ÿ” Audio sample rate: \(sampleRate) Hz") return sampleRate } } return 0 } catch { - print("โŒ Failed to get audio sample rate: \(error.localizedDescription)") + PluginLog.print("โŒ Failed to get audio sample rate: \(error.localizedDescription)") return 0 } } @@ -248,7 +248,7 @@ internal class MediaInfoExtractor { static func getVideoFormatInfo(_ videoPath: String) async -> VideoFormatInfo { let url = URL(fileURLWithPath: videoPath) guard FileManager.default.fileExists(atPath: url.path) else { - print("โŒ Video file does not exist: \(videoPath)") + PluginLog.print("โŒ Video file does not exist: \(videoPath)") return VideoFormatInfo(isHevc: false, bitDepth: 8, isHdr: false, profile: nil) } @@ -323,7 +323,7 @@ internal class MediaInfoExtractor { } } - print( + PluginLog.print( "๐Ÿ” Video format: path=\(videoPath), isHevc=\(isHevc), bitDepth=\(bitDepth), isHdr=\(isHdr), profile=\(profile ?? "unknown")" ) @@ -331,7 +331,7 @@ internal class MediaInfoExtractor { isHevc: isHevc, bitDepth: bitDepth, isHdr: isHdr, profile: profile) } catch { - print( + PluginLog.print( "โŒ Failed to get video format info for \(videoPath): \(error.localizedDescription)") return VideoFormatInfo(isHevc: false, bitDepth: 8, isHdr: false, profile: nil) } diff --git a/ios/Classes/src/features/render/helpers/VideoSequenceBuilder.swift b/ios/Classes/src/features/render/helpers/VideoSequenceBuilder.swift index 0963d9f..56f93b3 100644 --- a/ios/Classes/src/features/render/helpers/VideoSequenceBuilder.swift +++ b/ios/Classes/src/features/render/helpers/VideoSequenceBuilder.swift @@ -38,7 +38,7 @@ internal class VideoSequenceBuilder { } let durationMs = Int(totalDuration.seconds * 1000) - print("๐Ÿ” Total video duration: \(durationMs) ms") + PluginLog.print("๐Ÿ” Total video duration: \(durationMs) ms") return totalDuration } @@ -77,8 +77,8 @@ internal class VideoSequenceBuilder { ) } - print("๐ŸŽฌ Building video sequence with \(videoClips.count) clips") - print("๐Ÿ”Š Audio enabled: \(enableAudio)") + PluginLog.print("๐ŸŽฌ Building video sequence with \(videoClips.count) clips") + PluginLog.print("๐Ÿ”Š Audio enabled: \(enableAudio)") var totalDuration = CMTime.zero var maxRenderSize = CGSize.zero @@ -108,17 +108,17 @@ internal class VideoSequenceBuilder { preferredTrackID: kCMPersistentTrackID_Invalid ) if sharedAudioTrack != nil { - print("๐Ÿ”Š Created SHARED audio track for all clips (will prevent empty segments)") + PluginLog.print("๐Ÿ”Š Created SHARED audio track for all clips (will prevent empty segments)") } } // Process each video clip for (index, clip) in videoClips.enumerated() { - print("๐Ÿ“น Processing clip \(index): \(clip.inputPath)") + PluginLog.print("๐Ÿ“น Processing clip \(index): \(clip.inputPath)") let url = URL(fileURLWithPath: clip.inputPath) guard FileManager.default.fileExists(atPath: url.path) else { - print("โŒ ERROR: Video file does not exist: \(clip.inputPath)") + PluginLog.print("โŒ ERROR: Video file does not exist: \(clip.inputPath)") throw NSError( domain: "VideoSequenceBuilder", code: 3, @@ -148,13 +148,13 @@ internal class VideoSequenceBuilder { // Log video properties let angle = atan2(preferredTransform.b, preferredTransform.a) let degrees = angle * 180 / .pi - print("๐Ÿ“น Clip \(index) properties:") - print(" - Natural size: \(naturalSize.width) x \(naturalSize.height)") - print( + PluginLog.print("๐Ÿ“น Clip \(index) properties:") + PluginLog.print(" - Natural size: \(naturalSize.width) x \(naturalSize.height)") + PluginLog.print( " - Rotation: \(degrees)ยฐ (transform: [\(preferredTransform.a), \(preferredTransform.b), \(preferredTransform.c), \(preferredTransform.d), \(preferredTransform.tx), \(preferredTransform.ty)])" ) - print(" - Display size: \(correctedSize.width) x \(correctedSize.height)") - print(" - Frame rate: \(nominalFrameRate) fps") + PluginLog.print(" - Display size: \(correctedSize.width) x \(correctedSize.height)") + PluginLog.print(" - Frame rate: \(nominalFrameRate) fps") // Update max render size if correctedSize.width > maxRenderSize.width @@ -162,7 +162,7 @@ internal class VideoSequenceBuilder { { let oldSize = maxRenderSize maxRenderSize = correctedSize - print( + PluginLog.print( " - โฌ†๏ธ Max render size updated: \(oldSize.width)x\(oldSize.height) โ†’ \(maxRenderSize.width)x\(maxRenderSize.height)" ) } @@ -197,13 +197,13 @@ internal class VideoSequenceBuilder { let audioTrack = try? await MediaInfoExtractor.loadAudioTrack(from: asset), let sharedAudioTrack = sharedAudioTrack { - print("๐Ÿ”Š Processing audio for clip \(index)...") - print(" โœ… Audio track loaded from asset") - print(" Track ID: \(audioTrack.trackID)") - print( + PluginLog.print("๐Ÿ”Š Processing audio for clip \(index)...") + PluginLog.print(" โœ… Audio track loaded from asset") + PluginLog.print(" Track ID: \(audioTrack.trackID)") + PluginLog.print( " Duration: \(String(format: "%.2f", audioTrack.timeRange.duration.seconds))s" ) - print(" Format: \(audioTrack.mediaType)") + PluginLog.print(" Format: \(audioTrack.mediaType)") do { try sharedAudioTrack.insertTimeRange( @@ -211,52 +211,52 @@ internal class VideoSequenceBuilder { of: audioTrack, at: totalDuration ) - print(" โœ… Audio inserted into SHARED track!") - print( + PluginLog.print(" โœ… Audio inserted into SHARED track!") + PluginLog.print( " Source time range: \(String(format: "%.2f", clipTimeRange.start.seconds))s - \(String(format: "%.2f", (clipTimeRange.start + clipTimeRange.duration).seconds))s" ) - print( + PluginLog.print( " Inserted at composition time: \(String(format: "%.2f", totalDuration.seconds))s" ) - print( + PluginLog.print( " Audio duration: \(String(format: "%.2f", clipTimeRange.duration.seconds))s" ) } catch { - print(" โŒ ERROR inserting audio: \(error.localizedDescription)") - print(" Error details: \(error)") + PluginLog.print(" โŒ ERROR inserting audio: \(error.localizedDescription)") + PluginLog.print(" Error details: \(error)") } } totalDuration = CMTimeAdd(totalDuration, clipDuration) - print("โœ… Clip \(index) added successfully") - print(" - Duration: \(String(format: "%.2f", clipDuration.seconds))s") - print( + PluginLog.print("โœ… Clip \(index) added successfully") + PluginLog.print(" - Duration: \(String(format: "%.2f", clipDuration.seconds))s") + PluginLog.print( " - Time range in composition: \(String(format: "%.2f", totalDuration.seconds - clipDuration.seconds))s - \(String(format: "%.2f", totalDuration.seconds))s" ) } - print("") - print("๐Ÿ“Š ===== VIDEO SEQUENCE SUMMARY =====") - print(" Total clips: \(videoClips.count)") - print(" Total duration: \(String(format: "%.2f", totalDuration.seconds))s") - print(" Max render size: \(maxRenderSize.width) x \(maxRenderSize.height)") - print(" Max frame rate: \(maxFrameRate) fps") - print(" Clip instructions: \(clipInstructions.count)") + PluginLog.print("") + PluginLog.print("๐Ÿ“Š ===== VIDEO SEQUENCE SUMMARY =====") + PluginLog.print(" Total clips: \(videoClips.count)") + PluginLog.print(" Total duration: \(String(format: "%.2f", totalDuration.seconds))s") + PluginLog.print(" Max render size: \(maxRenderSize.width) x \(maxRenderSize.height)") + PluginLog.print(" Max frame rate: \(maxFrameRate) fps") + PluginLog.print(" Clip instructions: \(clipInstructions.count)") // Handle shared audio track - add to result if it has segments, otherwise remove from composition if let audioTrack = sharedAudioTrack { if !audioTrack.segments.isEmpty { originalAudioTracks.append(audioTrack) } else { - print(" โš ๏ธ Shared audio track has no segments - removing from composition") + PluginLog.print(" โš ๏ธ Shared audio track has no segments - removing from composition") composition.removeTrack(audioTrack) } } else { - print(" ๐Ÿ”Š AUDIO TRACKS: 0 (no audio track created)") + PluginLog.print(" ๐Ÿ”Š AUDIO TRACKS: 0 (no audio track created)") } - print("=====================================") - print("") + PluginLog.print("=====================================") + PluginLog.print("") return VideoSequenceResult( videoTrack: compositionVideoTrack, diff --git a/ios/Classes/src/features/render/helpers/VideoTranscoder.swift b/ios/Classes/src/features/render/helpers/VideoTranscoder.swift index 1dc9687..9def5d1 100644 --- a/ios/Classes/src/features/render/helpers/VideoTranscoder.swift +++ b/ios/Classes/src/features/render/helpers/VideoTranscoder.swift @@ -33,7 +33,7 @@ internal class VideoTranscoder { let formatInfo = await MediaInfoExtractor.getVideoFormatInfo(videoPath) let needsTranscode = formatInfo.needsTranscodingForEffects() - print( + PluginLog.print( "๐Ÿ” Video transcoding check: path=\(videoPath), " + "isHevc=\(formatInfo.isHevc), bitDepth=\(formatInfo.bitDepth), " + "isHdr=\(formatInfo.isHdr), needsTranscoding=\(needsTranscode)") @@ -51,11 +51,11 @@ internal class VideoTranscoder { static func transcodeToH264(_ videoPath: String) async -> TranscodeResult { // Check if transcoding is needed guard await needsTranscoding(videoPath) else { - print("โœ… No transcoding needed for: \(videoPath)") + PluginLog.print("โœ… No transcoding needed for: \(videoPath)") return .notNeeded(originalPath: videoPath) } - print("๐ŸŽฌ Starting HEVC 10-bit HDR โ†’ H.264 8-bit SDR transcoding for: \(videoPath)") + PluginLog.print("๐ŸŽฌ Starting HEVC 10-bit HDR โ†’ H.264 8-bit SDR transcoding for: \(videoPath)") let inputURL = URL(fileURLWithPath: videoPath) let outputURL = FileManager.default.temporaryDirectory @@ -66,15 +66,15 @@ internal class VideoTranscoder { // Verify output let outputInfo = await MediaInfoExtractor.getVideoFormatInfo(outputURL.path) - print("โœ… Transcoding completed: \(outputURL.path)") - print( + PluginLog.print("โœ… Transcoding completed: \(outputURL.path)") + PluginLog.print( " Output: isHevc=\(outputInfo.isHevc), bitDepth=\(outputInfo.bitDepth), isHdr=\(outputInfo.isHdr)" ) return .success(outputPath: outputURL.path) } catch { - print("โŒ Transcoding failed: \(error.localizedDescription)") + PluginLog.print("โŒ Transcoding failed: \(error.localizedDescription)") try? FileManager.default.removeItem(at: outputURL) return .error(error) } @@ -94,7 +94,7 @@ internal class VideoTranscoder { case .notNeeded(let originalPath): result[inputPath] = originalPath case .error: - print("โš ๏ธ Transcoding failed for \(inputPath), using original") + PluginLog.print("โš ๏ธ Transcoding failed for \(inputPath), using original") result[inputPath] = inputPath } } @@ -110,9 +110,9 @@ internal class VideoTranscoder { if path.contains("transcoded_") { do { try FileManager.default.removeItem(atPath: path) - print("๐Ÿ—‘๏ธ Cleaned up transcoded file: \(path)") + PluginLog.print("๐Ÿ—‘๏ธ Cleaned up transcoded file: \(path)") } catch { - print("โš ๏ธ Failed to clean up \(path): \(error.localizedDescription)") + PluginLog.print("โš ๏ธ Failed to clean up \(path): \(error.localizedDescription)") } } } @@ -236,8 +236,8 @@ internal class VideoTranscoder { exportSession.videoComposition = videoComposition } - print("๐ŸŽฌ Transcoding with AVAssetExportSession...") - print(" Input size: \(naturalSize), Output size: \(renderSize)") + PluginLog.print("๐ŸŽฌ Transcoding with AVAssetExportSession...") + PluginLog.print(" Input size: \(naturalSize), Output size: \(renderSize)") // Export if #available(iOS 18.0, *) { @@ -252,7 +252,7 @@ internal class VideoTranscoder { } } - print("โœ… Transcoding completed successfully") + PluginLog.print("โœ… Transcoding completed successfully") } /// Calculates output size accounting for rotation. diff --git a/ios/Classes/src/features/thumbnail/ThumbnailGenerator.swift b/ios/Classes/src/features/thumbnail/ThumbnailGenerator.swift index 71d5720..1868e8e 100644 --- a/ios/Classes/src/features/thumbnail/ThumbnailGenerator.swift +++ b/ios/Classes/src/features/thumbnail/ThumbnailGenerator.swift @@ -82,7 +82,7 @@ class ThumbnailGenerator { let key = requestedTime.seconds guard let index = timeIndexMap[key] else { - print("โš ๏ธ Unexpected time: \(Int(key * 1000)) ms") + PluginLog.print("โš ๏ธ Unexpected time: \(Int(key * 1000)) ms") return } @@ -99,12 +99,12 @@ class ThumbnailGenerator { resultData[index] = data let elapsed = Int((Date().timeIntervalSince1970 - start) * 1000) - print( + PluginLog.print( "[\(index)] โœ… \(Int(key * 1000)) ms in \(elapsed) ms (\(data.count) bytes)" ) } else { let message = error?.localizedDescription ?? "Unknown error" - print("[\(index)] โŒ Failed at \(Int(key * 1000)) ms: \(message)") + PluginLog.print("[\(index)] โŒ Failed at \(Int(key * 1000)) ms: \(message)") } completed += 1 @@ -197,7 +197,7 @@ class ThumbnailGenerator { case "jpeg", "jpg": return image.jpegData(compressionQuality: quality) ?? Data() default: - print("โš ๏ธ Format \(format) not supported, falling back to JPEG") + PluginLog.print("โš ๏ธ Format \(format) not supported, falling back to JPEG") return image.jpegData(compressionQuality: quality) ?? Data() } } @@ -220,7 +220,7 @@ class ThumbnailGenerator { do { duration = try await asset.load(.duration) } catch { - print("โŒ Failed to load duration: \(error.localizedDescription)") + PluginLog.print("โŒ Failed to load duration: \(error.localizedDescription)") return [] } } else { diff --git a/ios/Classes/src/shared/logging/PluginLog.swift b/ios/Classes/src/shared/logging/PluginLog.swift new file mode 100644 index 0000000..4256f18 --- /dev/null +++ b/ios/Classes/src/shared/logging/PluginLog.swift @@ -0,0 +1,91 @@ +import Foundation + +enum PluginLogLevel: Int { + case verbose = 0 + case debug = 1 + case info = 2 + case warning = 3 + case error = 4 + case none = 5 + + static var `default`: PluginLogLevel { + #if DEBUG + return .debug + #else + return .warning + #endif + } + + static func from(methodValue: String) throws -> PluginLogLevel { + switch methodValue.lowercased() { + case "verbose": + return .verbose + case "debug": + return .debug + case "info": + return .info + case "warning", "warn": + return .warning + case "error": + return .error + case "none": + return .none + default: + throw PluginLogError.unsupportedLevel(methodValue) + } + } +} + +enum PluginLogError: LocalizedError { + case unsupportedLevel(String) + + var errorDescription: String? { + switch self { + case .unsupportedLevel(let level): + return "Unsupported native log level: \(level)" + } + } +} + +enum PluginLog { + private static var minimumLevel = PluginLogLevel.default + + static func setMinimumLevel(_ methodValue: String) throws { + minimumLevel = try .from(methodValue: methodValue) + } + + static func print( + _ items: Any..., separator: String = " ", terminator: String = "\n" + ) { + let message = items.map { String(describing: $0) }.joined(separator: separator) + log(message, terminator: terminator) + } + + private static func log(_ message: String, terminator: String) { + let level = inferredLevel(for: message) + guard shouldLog(level) else { return } + Swift.print(message, terminator: terminator) + } + + private static func shouldLog(_ level: PluginLogLevel) -> Bool { + level.rawValue >= minimumLevel.rawValue && minimumLevel != .none + } + + private static func inferredLevel(for message: String) -> PluginLogLevel { + if message.contains("โŒ") || message.localizedCaseInsensitiveContains("error") || + message.localizedCaseInsensitiveContains("failed") + { + return .error + } + + if message.contains("โš ๏ธ") || message.localizedCaseInsensitiveContains("warn") { + return .warning + } + + if message.contains("โ„น๏ธ") || message.localizedCaseInsensitiveContains("info") { + return .info + } + + return .debug + } +} \ No newline at end of file diff --git a/lib/core/models/platform/native_log_level.dart b/lib/core/models/platform/native_log_level.dart new file mode 100644 index 0000000..a178ec1 --- /dev/null +++ b/lib/core/models/platform/native_log_level.dart @@ -0,0 +1,25 @@ +/// Controls how much native logging the plugin emits on supported platforms. +enum NativeLogLevel { + /// Disables all native logs from the plugin. + none('none'), + + /// Emits only error logs. + error('error'), + + /// Emits warnings and errors. + warning('warning'), + + /// Emits informational messages, warnings, and errors. + info('info'), + + /// Emits debug, info, warning, and error logs. + debug('debug'), + + /// Emits all available logs, including verbose output. + verbose('verbose'); + + const NativeLogLevel(this.methodValue); + + /// String value sent over the platform channel. + final String methodValue; +} diff --git a/lib/core/platform/native_method_channel.dart b/lib/core/platform/native_method_channel.dart index 0e1cd77..ad232fb 100644 --- a/lib/core/platform/native_method_channel.dart +++ b/lib/core/platform/native_method_channel.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:mime/mime.dart'; import 'package:pro_video_editor/core/models/exceptions/audio_exceptions.dart'; +import 'package:pro_video_editor/core/models/platform/native_log_level.dart'; import '/core/models/audio/audio_extract_configs_model.dart'; import '/core/models/audio/waveform_chunk_model.dart'; @@ -66,13 +67,15 @@ class MethodChannelProVideoEditor extends ProVideoEditor { /// /// Emits [WaveformChunk] events during streaming waveform generation, /// allowing progressive UI updates. - final _waveformStreamChannel = - const EventChannel('pro_video_editor_waveform_stream'); + final _waveformStreamChannel = const EventChannel( + 'pro_video_editor_waveform_stream', + ); @override Future getPlatformVersion() async { - final version = - await methodChannel.invokeMethod('getPlatformVersion'); + final version = await methodChannel.invokeMethod( + 'getPlatformVersion', + ); return version; } @@ -80,6 +83,7 @@ class MethodChannelProVideoEditor extends ProVideoEditor { Future getMetadata( EditorVideo value, { bool checkStreamingOptimization = false, + NativeLogLevel? nativeLogLevel, }) async { var inputPath = await value.safeFilePath(); @@ -90,6 +94,7 @@ class MethodChannelProVideoEditor extends ProVideoEditor { 'inputPath': inputPath, 'extension': extension, 'checkStreamingOptimization': checkStreamingOptimization, + 'nativeLogLevel': nativeLogLevel?.methodValue, }) ?? {}; @@ -97,53 +102,66 @@ class MethodChannelProVideoEditor extends ProVideoEditor { } @override - Future hasAudioTrack(EditorVideo value) async { + Future hasAudioTrack( + EditorVideo value, { + NativeLogLevel? nativeLogLevel, + }) async { var inputPath = await value.safeFilePath(); - final result = await methodChannel.invokeMethod( - 'hasAudioTrack', - { - 'inputPath': inputPath, - 'extension': _getFileExtension(inputPath), - }, - ); + final result = await methodChannel.invokeMethod('hasAudioTrack', { + 'inputPath': inputPath, + 'extension': _getFileExtension(inputPath), + 'nativeLogLevel': nativeLogLevel?.methodValue, + }); return result ?? false; } - Future> _extractThumbnails(ThumbnailBase value) async { + Future> _extractThumbnails( + ThumbnailBase value, { + NativeLogLevel? nativeLogLevel, + }) async { var inputPath = await value.video.safeFilePath(); - final response = await methodChannel.invokeMethod>( - 'getThumbnails', - { - 'inputPath': inputPath, - 'extension': _getFileExtension(inputPath), - ...value.toMap(), - }, - ); + final response = + await methodChannel.invokeMethod>('getThumbnails', { + 'inputPath': inputPath, + 'extension': _getFileExtension(inputPath), + 'nativeLogLevel': nativeLogLevel?.methodValue, + ...value.toMap(), + }); final List result = response?.cast() ?? []; return result; } @override - Future> getThumbnails(ThumbnailConfigs value) async { - return await _extractThumbnails(value); + Future> getThumbnails( + ThumbnailConfigs value, { + NativeLogLevel? nativeLogLevel, + }) async { + return await _extractThumbnails(value, nativeLogLevel: nativeLogLevel); } @override - Future> getKeyFrames(KeyFramesConfigs value) async { - return await _extractThumbnails(value); + Future> getKeyFrames( + KeyFramesConfigs value, { + NativeLogLevel? nativeLogLevel, + }) async { + return await _extractThumbnails(value, nativeLogLevel: nativeLogLevel); } @override - Future getSingleThumbnail(SingleThumbnailConfigs value) async { + Future getSingleThumbnail( + SingleThumbnailConfigs value, { + NativeLogLevel? nativeLogLevel, + }) async { final bool isLastFrame = value.position == ThumbnailPosition.last; Duration timestamp; if (isLastFrame) { - final duration = - value.videoDuration ?? (await getMetadata(value.video)).duration; + final duration = value.videoDuration ?? + (await getMetadata(value.video, nativeLogLevel: nativeLogLevel)) + .duration; timestamp = duration; } else { timestamp = Duration.zero; @@ -156,6 +174,7 @@ class MethodChannelProVideoEditor extends ProVideoEditor { { 'inputPath': inputPath, 'extension': _getFileExtension(inputPath), + 'nativeLogLevel': nativeLogLevel?.methodValue, ...value.toMap(), 'timestamps': [timestamp.inMicroseconds], 'lastFrameTolerance': isLastFrame, @@ -166,18 +185,20 @@ class MethodChannelProVideoEditor extends ProVideoEditor { } @override - Future extractAudio(AudioExtractConfigs value) async { + Future extractAudio( + AudioExtractConfigs value, { + NativeLogLevel? nativeLogLevel, + }) async { try { var inputPath = await value.video.safeFilePath(); - final Uint8List? result = await methodChannel.invokeMethod( - 'extractAudio', - { - 'inputPath': inputPath, - 'extension': _getFileExtension(inputPath), - ...value.toMap(), - }, - ); + final Uint8List? result = + await methodChannel.invokeMethod('extractAudio', { + 'inputPath': inputPath, + 'extension': _getFileExtension(inputPath), + 'nativeLogLevel': nativeLogLevel?.methodValue, + ...value.toMap(), + }); if (result == null) { throw ArgumentError('Failed to extract audio from video'); @@ -197,20 +218,19 @@ class MethodChannelProVideoEditor extends ProVideoEditor { @override Future extractAudioToFile( String filePath, - AudioExtractConfigs value, - ) async { + AudioExtractConfigs value, { + NativeLogLevel? nativeLogLevel, + }) async { try { var inputPath = await value.video.safeFilePath(); - await methodChannel.invokeMethod( - 'extractAudio', - { - 'inputPath': inputPath, - 'extension': _getFileExtension(inputPath), - 'outputPath': filePath, - ...value.toMap(), - }, - ); + await methodChannel.invokeMethod('extractAudio', { + 'inputPath': inputPath, + 'extension': _getFileExtension(inputPath), + 'nativeLogLevel': nativeLogLevel?.methodValue, + 'outputPath': filePath, + ...value.toMap(), + }); return filePath; } on PlatformException catch (error) { @@ -224,18 +244,20 @@ class MethodChannelProVideoEditor extends ProVideoEditor { } @override - Future getWaveform(WaveformConfigs value) async { + Future getWaveform( + WaveformConfigs value, { + NativeLogLevel? nativeLogLevel, + }) async { try { var inputPath = await value.video.safeFilePath(); - final response = await methodChannel.invokeMethod>( - 'getWaveform', - { - 'inputPath': inputPath, - 'extension': _getFileExtension(inputPath), - ...value.toMap(), - }, - ); + final response = await methodChannel + .invokeMethod>('getWaveform', { + 'inputPath': inputPath, + 'extension': _getFileExtension(inputPath), + 'nativeLogLevel': nativeLogLevel?.methodValue, + ...value.toMap(), + }); if (response == null) { throw ArgumentError('Failed to generate waveform data'); @@ -253,21 +275,22 @@ class MethodChannelProVideoEditor extends ProVideoEditor { } @override - Stream getWaveformStream(WaveformConfigs value) async* { + Stream getWaveformStream( + WaveformConfigs value, { + NativeLogLevel? nativeLogLevel, + }) async* { // Get the input path before starting the stream final inputPath = await value.video.safeFilePath(); final extension = _getFileExtension(inputPath); // Start the streaming waveform generation on native side // The native side will start sending chunks via the event channel - await methodChannel.invokeMethod( - 'startWaveformStream', - { - 'inputPath': inputPath, - 'extension': extension, - ...value.toMap(), - }, - ); + await methodChannel.invokeMethod('startWaveformStream', { + 'inputPath': inputPath, + 'extension': extension, + 'nativeLogLevel': nativeLogLevel?.methodValue, + ...value.toMap(), + }); // Listen to the waveform stream event channel final streamController = StreamController(); @@ -290,10 +313,12 @@ class MethodChannelProVideoEditor extends ProVideoEditor { } else if (errorCode == renderCanceledErrorCode) { streamController.addError(const RenderCanceledException()); } else { - streamController.addError(PlatformException( - code: errorCode ?? 'WAVEFORM_ERROR', - message: error, - )); + streamController.addError( + PlatformException( + code: errorCode ?? 'WAVEFORM_ERROR', + message: error, + ), + ); } streamController.close(); subscription?.cancel(); @@ -339,13 +364,19 @@ class MethodChannelProVideoEditor extends ProVideoEditor { } @override - Future renderVideo(VideoRenderData value) async { + Future renderVideo( + VideoRenderData value, { + NativeLogLevel? nativeLogLevel, + }) async { try { final renderData = await value.toAsyncMap(); final Uint8List? result = await methodChannel.invokeMethod( 'renderVideo', - renderData, + { + ...renderData, + 'nativeLogLevel': nativeLogLevel?.methodValue, + }, ); if (result == null) { @@ -364,18 +395,17 @@ class MethodChannelProVideoEditor extends ProVideoEditor { @override Future renderVideoToFile( String filePath, - VideoRenderData value, - ) async { + VideoRenderData value, { + NativeLogLevel? nativeLogLevel, + }) async { try { final renderData = await value.toAsyncMap(); - await methodChannel.invokeMethod( - 'renderVideo', - { - ...renderData, - 'outputPath': filePath, - }, - ); + await methodChannel.invokeMethod('renderVideo', { + ...renderData, + 'outputPath': filePath, + 'nativeLogLevel': nativeLogLevel?.methodValue, + }); return filePath; } on PlatformException catch (error) { @@ -392,12 +422,7 @@ class MethodChannelProVideoEditor extends ProVideoEditor { throw ArgumentError('taskId cannot be empty'); } - await methodChannel.invokeMethod( - 'cancelTask', - { - 'id': taskId, - }, - ); + await methodChannel.invokeMethod('cancelTask', {'id': taskId}); } @override diff --git a/lib/core/platform/platform_interface.dart b/lib/core/platform/platform_interface.dart index d1dfa72..424087f 100644 --- a/lib/core/platform/platform_interface.dart +++ b/lib/core/platform/platform_interface.dart @@ -7,6 +7,7 @@ import '/core/models/audio/audio_extract_configs_model.dart'; import '/core/models/audio/waveform_chunk_model.dart'; import '/core/models/audio/waveform_configs_model.dart'; import '/core/models/audio/waveform_data_model.dart'; +import '/core/models/platform/native_log_level.dart'; import '/core/models/thumbnail/key_frames_configs_model.dart'; import '/core/models/thumbnail/single_thumbnail_configs_model.dart'; import '/core/models/thumbnail/thumbnail_configs_model.dart'; @@ -113,6 +114,7 @@ abstract class ProVideoEditor extends PlatformInterface { Future getMetadata( EditorVideo value, { bool checkStreamingOptimization = false, + NativeLogLevel? nativeLogLevel, }) { throw UnimplementedError('getMetadata() has not been implemented.'); } @@ -147,7 +149,10 @@ abstract class ProVideoEditor extends PlatformInterface { /// print('Video has no audio track'); /// } /// ``` - Future hasAudioTrack(EditorVideo value) { + Future hasAudioTrack( + EditorVideo value, { + NativeLogLevel? nativeLogLevel, + }) { throw UnimplementedError('hasAudioTrack() has not been implemented.'); } @@ -167,7 +172,10 @@ abstract class ProVideoEditor extends PlatformInterface { /// /// Progress updates are emitted via [progressStreamById] using the task ID /// from [ThumbnailConfigs.id]. - Future> getThumbnails(ThumbnailConfigs value) { + Future> getThumbnails( + ThumbnailConfigs value, { + NativeLogLevel? nativeLogLevel, + }) { throw UnimplementedError('getThumbnails() has not been implemented.'); } @@ -187,7 +195,10 @@ abstract class ProVideoEditor extends PlatformInterface { /// /// Progress updates are emitted via [progressStreamById] using the task ID /// from [KeyFramesConfigs.id]. - Future> getKeyFrames(KeyFramesConfigs value) { + Future> getKeyFrames( + KeyFramesConfigs value, { + NativeLogLevel? nativeLogLevel, + }) { throw UnimplementedError('getKeyFrames() has not been implemented.'); } @@ -218,7 +229,10 @@ abstract class ProVideoEditor extends PlatformInterface { /// final thumbnail = /// await ProVideoEditor.instance.getSingleThumbnail(config); /// ``` - Future getSingleThumbnail(SingleThumbnailConfigs value) { + Future getSingleThumbnail( + SingleThumbnailConfigs value, { + NativeLogLevel? nativeLogLevel, + }) { throw UnimplementedError('getSingleThumbnail() has not been implemented.'); } @@ -268,7 +282,10 @@ abstract class ProVideoEditor extends PlatformInterface { /// /// final audioData = await ProVideoEditor.instance.extractAudio(config); /// ``` - Future extractAudio(AudioExtractConfigs value) { + Future extractAudio( + AudioExtractConfigs value, { + NativeLogLevel? nativeLogLevel, + }) { throw UnimplementedError('extractAudio() has not been implemented.'); } @@ -307,8 +324,9 @@ abstract class ProVideoEditor extends PlatformInterface { /// ``` Future extractAudioToFile( String filePath, - AudioExtractConfigs value, - ) { + AudioExtractConfigs value, { + NativeLogLevel? nativeLogLevel, + }) { throw UnimplementedError('extractAudioToFile() has not been implemented.'); } @@ -356,7 +374,10 @@ abstract class ProVideoEditor extends PlatformInterface { /// final waveform = await ProVideoEditor.instance.getWaveform(configs); /// print('Generated ${waveform.sampleCount} samples'); /// ``` - Future getWaveform(WaveformConfigs value) { + Future getWaveform( + WaveformConfigs value, { + NativeLogLevel? nativeLogLevel, + }) { throw UnimplementedError('getWaveform() has not been implemented.'); } @@ -406,7 +427,10 @@ abstract class ProVideoEditor extends PlatformInterface { /// } /// } /// ``` - Stream getWaveformStream(WaveformConfigs value) { + Stream getWaveformStream( + WaveformConfigs value, { + NativeLogLevel? nativeLogLevel, + }) { throw UnimplementedError('getWaveformStream() has not been implemented.'); } @@ -434,7 +458,10 @@ abstract class ProVideoEditor extends PlatformInterface { /// /// Progress updates are emitted via [progressStreamById] using /// [VideoRenderData.id]. - Future renderVideo(VideoRenderData value) { + Future renderVideo( + VideoRenderData value, { + NativeLogLevel? nativeLogLevel, + }) { throw UnimplementedError('renderVideo() has not been implemented.'); } @@ -466,8 +493,9 @@ abstract class ProVideoEditor extends PlatformInterface { /// [VideoRenderData.id]. Future renderVideoToFile( String filePath, - VideoRenderData value, - ) { + VideoRenderData value, { + NativeLogLevel? nativeLogLevel, + }) { throw UnimplementedError('renderVideoToFile() has not been implemented.'); } diff --git a/lib/pro_video_editor.dart b/lib/pro_video_editor.dart index 0079cc3..2fb3683 100644 --- a/lib/pro_video_editor.dart +++ b/lib/pro_video_editor.dart @@ -6,6 +6,7 @@ export '/core/models/audio/audio_track_model.dart'; export '/core/models/audio/waveform_chunk_model.dart'; export '/core/models/audio/waveform_configs_model.dart'; export '/core/models/audio/waveform_data_model.dart'; +export '/core/models/platform/native_log_level.dart'; export 'features/audio/widgets/audio_waveform.dart'; export 'features/audio/models/waveform_style.dart'; export 'core/models/image/editor_layer_image_model.dart'; diff --git a/macos/Classes/ProVideoEditorPlugin.swift b/macos/Classes/ProVideoEditorPlugin.swift index db4532c..593c923 100644 --- a/macos/Classes/ProVideoEditorPlugin.swift +++ b/macos/Classes/ProVideoEditorPlugin.swift @@ -50,6 +50,8 @@ public class ProVideoEditorPlugin: NSObject, FlutterPlugin { /// - extractAudio: Extracts audio from video /// - cancelTask: Cancels active render or audio extraction task public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + applyInlineNativeLogLevel(call: call) + switch call.method { case "getPlatformVersion": handleGetPlatformVersion(result: result) @@ -83,6 +85,21 @@ public class ProVideoEditorPlugin: NSObject, FlutterPlugin { } } + private func applyInlineNativeLogLevel(call: FlutterMethodCall) { + guard let args = call.arguments as? [String: Any], + let level = args["nativeLogLevel"] as? String, + !level.isEmpty + else { + return + } + + do { + try PluginLog.setMinimumLevel(level) + } catch { + PluginLog.print("โš ๏ธ Ignoring invalid nativeLogLevel '\(level)': \(error.localizedDescription)") + } + } + // MARK: - Handler Methods /// Returns the macOS platform version string. diff --git a/macos/Classes/src/features/render/RenderVideo.swift b/macos/Classes/src/features/render/RenderVideo.swift index 1ce405f..4c51432 100644 --- a/macos/Classes/src/features/render/RenderVideo.swift +++ b/macos/Classes/src/features/render/RenderVideo.swift @@ -56,7 +56,7 @@ class RenderVideo { // HEVC 10-bit HDR videos cause issues with AVFoundation's compositor // They must be pre-transcoded to H.264 8-bit SDR for ANY effect processing - print("๐Ÿ” Checking for HEVC 10-bit videos that need transcoding...") + PluginLog.print("๐Ÿ” Checking for HEVC 10-bit videos that need transcoding...") // Pre-transcode HEVC 10-bit HDR videos to H.264 8-bit SDR let inputPaths = config.videoClips.map { $0.inputPath } @@ -66,7 +66,7 @@ class RenderVideo { transcodedFiles = transcodeMap.values.filter { $0.contains("transcoded_") } if !transcodedFiles.isEmpty { - print("โœ… Pre-transcoded \(transcodedFiles.count) HEVC 10-bit videos to H.264") + PluginLog.print("โœ… Pre-transcoded \(transcodedFiles.count) HEVC 10-bit videos to H.264") // Update config with transcoded paths let updatedClips = config.videoClips.map { clip -> VideoClip in @@ -109,10 +109,10 @@ class RenderVideo { let requestedFormat = workingConfig.outputFormat.lowercased() if pathExtension != requestedFormat { - print( + PluginLog.print( "โš ๏ธ WARNING: Output path extension '.\(pathExtension)' doesn't match requested format '.\(requestedFormat)'" ) - print("โš ๏ธ Correcting file extension to match format...") + PluginLog.print("โš ๏ธ Correcting file extension to match format...") // Replace extension with correct format let pathWithoutExtension = url.deletingPathExtension() @@ -124,16 +124,16 @@ class RenderVideo { outputURL = temporaryURL(for: workingConfig.outputFormat) } - print("") - print("๐ŸŽฌ ===== RENDER CONFIG =====") - print(" Video clips: \(workingConfig.videoClips.count)") - print(" ๐Ÿ“ Output format: \(workingConfig.outputFormat)") - print(" ๐Ÿ“น Output path: \(outputURL.path)") - print(" ๐Ÿ”Š Enable Audio: \(workingConfig.enableAudio)") - print(" ๐ŸŽต Audio tracks: \(workingConfig.audioTracks.count)") - print(" ๐ŸŽจ Color filters: \(workingConfig.colorFilters.count)") - print("===========================") - print("") + PluginLog.print("") + PluginLog.print("๐ŸŽฌ ===== RENDER CONFIG =====") + PluginLog.print(" Video clips: \(workingConfig.videoClips.count)") + PluginLog.print(" ๐Ÿ“ Output format: \(workingConfig.outputFormat)") + PluginLog.print(" ๐Ÿ“น Output path: \(outputURL.path)") + PluginLog.print(" ๐Ÿ”Š Enable Audio: \(workingConfig.enableAudio)") + PluginLog.print(" ๐ŸŽต Audio tracks: \(workingConfig.audioTracks.count)") + PluginLog.print(" ๐ŸŽจ Color filters: \(workingConfig.colorFilters.count)") + PluginLog.print("===========================") + PluginLog.print("") // Create configuration for video effects var effectsConfig = VideoCompositorConfig() @@ -345,10 +345,10 @@ class RenderVideo { } let fileType = mapFormatToMimeType(format: outputFormat) - print("๐Ÿ“น Export session setup:") - print(" - Requested format: \(outputFormat)") - print(" - AVFileType: \(fileType.rawValue)") - print(" - Output URL: \(outputURL.path)") + PluginLog.print("๐Ÿ“น Export session setup:") + PluginLog.print(" - Requested format: \(outputFormat)") + PluginLog.print(" - AVFileType: \(fileType.rawValue)") + PluginLog.print(" - Output URL: \(outputURL.path)") export.outputURL = outputURL export.outputFileType = fileType @@ -373,7 +373,7 @@ class RenderVideo { if CMTimeGetSeconds(clampedDuration) > 0 { export.timeRange = CMTimeRange(start: startTime, duration: clampedDuration) - print( + PluginLog.print( " - TimeRange applied: \(String(format: "%.2f", CMTimeGetSeconds(startTime)))s - \(String(format: "%.2f", CMTimeGetSeconds(CMTimeAdd(startTime, clampedDuration))))s" ) } @@ -386,15 +386,15 @@ class RenderVideo { // Apply audio mix if available if let audioMix = audioMix, hasAudioTracks { export.audioMix = audioMix - print("๐Ÿ”Š Audio mix applied to export session") + PluginLog.print("๐Ÿ”Š Audio mix applied to export session") } else if !hasAudioTracks { - print("โ„น๏ธ No audio tracks in composition - exporting video only") + PluginLog.print("โ„น๏ธ No audio tracks in composition - exporting video only") } // Apply fast start optimization (moves moov atom to beginning for streaming) export.shouldOptimizeForNetworkUse = shouldOptimizeForNetworkUse if shouldOptimizeForNetworkUse { - print("๐Ÿš€ Fast start enabled - optimizing for network streaming") + PluginLog.print("๐Ÿš€ Fast start enabled - optimizing for network streaming") } return export diff --git a/macos/Classes/src/features/render/helpers/ApplyBitrate.swift b/macos/Classes/src/features/render/helpers/ApplyBitrate.swift index 63082cc..9eaf01b 100644 --- a/macos/Classes/src/features/render/helpers/ApplyBitrate.swift +++ b/macos/Classes/src/features/render/helpers/ApplyBitrate.swift @@ -26,10 +26,10 @@ import Foundation /// - <1 Mbps: Low quality public func applyBitrate(requestedBitrate: Int?, presetHint: String? = nil) -> String { if let bitrate = requestedBitrate { - print( + PluginLog.print( "[\(Tags.render)] ๐Ÿ“Š Requested bitrate: \(bitrate) bps (\(String(format: "%.1f", Double(bitrate) / 1_000_000)) Mbps)" ) - print( + PluginLog.print( "[\(Tags.render)] โš ๏ธ AVAssetExportSession does not support custom bitrate directly - using closest preset" ) } diff --git a/macos/Classes/src/features/render/helpers/ApplyBlur.swift b/macos/Classes/src/features/render/helpers/ApplyBlur.swift index 4b8ad30..2e878f4 100644 --- a/macos/Classes/src/features/render/helpers/ApplyBlur.swift +++ b/macos/Classes/src/features/render/helpers/ApplyBlur.swift @@ -18,5 +18,5 @@ func applyBlur( if sigma == nil || sigma == 0 { return } - print("[\(Tags.render)] ๐ŸŒซ๏ธ Applying Gaussian blur: sigma=\(String(format: "%.1f", sigma!))") + PluginLog.print("[\(Tags.render)] ๐ŸŒซ๏ธ Applying Gaussian blur: sigma=\(String(format: "%.1f", sigma!))") } diff --git a/macos/Classes/src/features/render/helpers/ApplyColorMatrix.swift b/macos/Classes/src/features/render/helpers/ApplyColorMatrix.swift index c7f0967..5e14478 100644 --- a/macos/Classes/src/features/render/helpers/ApplyColorMatrix.swift +++ b/macos/Classes/src/features/render/helpers/ApplyColorMatrix.swift @@ -25,7 +25,7 @@ func applyColorMatrix( let globalCount = filters.filter { $0.startUs == -1 && $0.endUs == -1 }.count let timedCount = filters.count - globalCount - print( + PluginLog.print( "[\(Tags.render)] ๐ŸŽจ Applying color grading: \(filters.count) filter(s) (\(globalCount) global, \(timedCount) timed)" ) } @@ -34,7 +34,7 @@ func applyColorMatrix( func multiplyColorMatrices(_ m1: [Double], _ m2: [Double]) -> [Double] { guard m1.count == 20, m2.count == 20 else { - print("Invalid matrix dimensions for multiplication") + PluginLog.print("Invalid matrix dimensions for multiplication") return m1 } diff --git a/macos/Classes/src/features/render/helpers/ApplyCrop.swift b/macos/Classes/src/features/render/helpers/ApplyCrop.swift index 1984dd6..fa39250 100644 --- a/macos/Classes/src/features/render/helpers/ApplyCrop.swift +++ b/macos/Classes/src/features/render/helpers/ApplyCrop.swift @@ -37,7 +37,7 @@ func applyCrop( config.cropHeight = height if cropX != 0 || cropY != 0 || cropWidth != nil || cropHeight != nil { - print( + PluginLog.print( "[\(Tags.render)] โœ‚๏ธ Applying crop: x=\(Int(x)), y=\(Int(y)), width=\(Int(width)), height=\(Int(height))" ) } diff --git a/macos/Classes/src/features/render/helpers/ApplyFlip.swift b/macos/Classes/src/features/render/helpers/ApplyFlip.swift index 0eecdd5..9fe2cf0 100644 --- a/macos/Classes/src/features/render/helpers/ApplyFlip.swift +++ b/macos/Classes/src/features/render/helpers/ApplyFlip.swift @@ -20,5 +20,5 @@ func applyFlip( if !flipX && !flipY { return } let flipType = flipX && flipY ? "both axes" : flipX ? "horizontal" : "vertical" - print("[\(Tags.render)] ๐Ÿ”„ Applying flip: \(flipType)") + PluginLog.print("[\(Tags.render)] ๐Ÿ”„ Applying flip: \(flipType)") } diff --git a/macos/Classes/src/features/render/helpers/ApplyImageLayer.swift b/macos/Classes/src/features/render/helpers/ApplyImageLayer.swift index a404511..dd06b81 100644 --- a/macos/Classes/src/features/render/helpers/ApplyImageLayer.swift +++ b/macos/Classes/src/features/render/helpers/ApplyImageLayer.swift @@ -26,6 +26,6 @@ func applyImageLayer( config.imageBytesWithCropping = withCropping if !imageLayers.isEmpty { - print("[\(Tags.render)] ๐Ÿ–ผ๏ธ Applying \(imageLayers.count) image layer(s) with timing") + PluginLog.print("[\(Tags.render)] ๐Ÿ–ผ๏ธ Applying \(imageLayers.count) image layer(s) with timing") } } diff --git a/macos/Classes/src/features/render/helpers/ApplyPlaybackSpeed.swift b/macos/Classes/src/features/render/helpers/ApplyPlaybackSpeed.swift index 031f236..440117b 100644 --- a/macos/Classes/src/features/render/helpers/ApplyPlaybackSpeed.swift +++ b/macos/Classes/src/features/render/helpers/ApplyPlaybackSpeed.swift @@ -25,7 +25,7 @@ public func applyPlaybackSpeed( guard let speed = speed, speed > 0, speed != 1 else { return instructions } let speedType = speed < 1 ? "slow motion" : "fast forward" - print( + PluginLog.print( "[\(Tags.render)] โšก Applying playback speed: \(String(format: "%.2f", speed))x (\(speedType))" ) diff --git a/macos/Classes/src/features/render/helpers/ApplyRotation.swift b/macos/Classes/src/features/render/helpers/ApplyRotation.swift index 6e0de12..04a8a27 100644 --- a/macos/Classes/src/features/render/helpers/ApplyRotation.swift +++ b/macos/Classes/src/features/render/helpers/ApplyRotation.swift @@ -28,5 +28,5 @@ func applyRotation( config.rotateTurns = turns if turns == 0 { return } - print("[\(Tags.render)] ๐Ÿ”„ Applying rotation: \(degrees)ยฐ (\(turns) ร— 90ยฐ)") + PluginLog.print("[\(Tags.render)] ๐Ÿ”„ Applying rotation: \(degrees)ยฐ (\(turns) ร— 90ยฐ)") } diff --git a/macos/Classes/src/features/render/helpers/ApplyScale.swift b/macos/Classes/src/features/render/helpers/ApplyScale.swift index 38ec20e..42ab7a4 100644 --- a/macos/Classes/src/features/render/helpers/ApplyScale.swift +++ b/macos/Classes/src/features/render/helpers/ApplyScale.swift @@ -25,6 +25,6 @@ func applyScale( if x != 1.0 || y != 1.0 { let percentX = Int(x * 100) let percentY = Int(y * 100) - print("[\(Tags.render)] ๐Ÿ“ Applying scale: X=\(percentX)%, Y=\(percentY)%") + PluginLog.print("[\(Tags.render)] ๐Ÿ“ Applying scale: X=\(percentX)%, Y=\(percentY)%") } } diff --git a/macos/Classes/src/features/render/helpers/AudioSequenceBuilder.swift b/macos/Classes/src/features/render/helpers/AudioSequenceBuilder.swift index 65761c0..9f22a65 100644 --- a/macos/Classes/src/features/render/helpers/AudioSequenceBuilder.swift +++ b/macos/Classes/src/features/render/helpers/AudioSequenceBuilder.swift @@ -101,7 +101,7 @@ internal class AudioSequenceBuilder { func build(in composition: AVMutableComposition) async throws -> AVMutableCompositionTrack? { let audioURL = URL(fileURLWithPath: audioPath) guard FileManager.default.fileExists(atPath: audioURL.path) else { - print("โš ๏ธ Custom audio file does not exist: \(audioPath)") + PluginLog.print("โš ๏ธ Custom audio file does not exist: \(audioPath)") return nil } @@ -113,7 +113,7 @@ internal class AudioSequenceBuilder { preferredTrackID: kCMPersistentTrackID_Invalid ) else { - print("โš ๏ธ Failed to add custom audio track") + PluginLog.print("โš ๏ธ Failed to add custom audio track") return nil } @@ -129,7 +129,7 @@ internal class AudioSequenceBuilder { let effectiveAudioEnd = audioEndTime ?? audioDuration let effectiveAudioDuration = CMTimeSubtract(effectiveAudioEnd, audioStartTime) if CMTimeCompare(effectiveAudioDuration, .zero) <= 0 { - print( + PluginLog.print( "โš ๏ธ Audio start/end time range is invalid (start: \(audioStartTime.seconds)s, end: \(effectiveAudioEnd.seconds)s)" ) return nil @@ -141,18 +141,18 @@ internal class AudioSequenceBuilder { let effectivePlayDuration = CMTimeMinimum(playDuration, remainingCompositionTime) if CMTimeCompare(effectivePlayDuration, .zero) <= 0 { - print("โš ๏ธ No time remaining in composition for audio track") + PluginLog.print("โš ๏ธ No time remaining in composition for audio track") return nil } if CMTimeCompare(audioStartTime, .zero) > 0 { - print("๐ŸŽต Custom audio start offset: \(audioStartTime.seconds)s") + PluginLog.print("๐ŸŽต Custom audio start offset: \(audioStartTime.seconds)s") } if audioEndTime != nil { - print("๐ŸŽต Custom audio end offset: \(effectiveAudioEnd.seconds)s") + PluginLog.print("๐ŸŽต Custom audio end offset: \(effectiveAudioEnd.seconds)s") } if CMTimeCompare(compositionInsertTime, .zero) > 0 { - print( + PluginLog.print( "๐ŸŽต Audio placed at composition time: \(compositionInsertTime.seconds)s" ) } @@ -163,7 +163,7 @@ internal class AudioSequenceBuilder { let timeRange = CMTimeRange(start: audioStartTime, duration: effectivePlayDuration) try compositionAudioTrack.insertTimeRange( timeRange, of: audioTrack, at: compositionInsertTime) - print("โœ‚๏ธ Custom audio trimmed to \(effectivePlayDuration.seconds)s") + PluginLog.print("โœ‚๏ธ Custom audio trimmed to \(effectivePlayDuration.seconds)s") } else if loopAudio { // Loop audio to match play duration var currentTime = compositionInsertTime @@ -188,7 +188,7 @@ internal class AudioSequenceBuilder { isFirstLoop = false } - print( + PluginLog.print( "๐Ÿ”„ Custom audio looped \(loopCount) times to match \(effectivePlayDuration.seconds)s duration" ) } else { @@ -197,14 +197,14 @@ internal class AudioSequenceBuilder { let timeRange = CMTimeRange(start: audioStartTime, duration: insertDuration) try compositionAudioTrack.insertTimeRange( timeRange, of: audioTrack, at: compositionInsertTime) - print( + PluginLog.print( "โ–ถ๏ธ Custom audio plays once (\(insertDuration.seconds)s, no loop)" + (CMTimeCompare(audioStartTime, .zero) > 0 ? " starting at \(audioStartTime.seconds)s" : "")) } if volume != 1.0 { - print("๐Ÿ”Š Custom audio volume: \(volume)") + PluginLog.print("๐Ÿ”Š Custom audio volume: \(volume)") } return compositionAudioTrack @@ -218,7 +218,7 @@ internal class AudioSequenceBuilder { let customSampleRate = await MediaInfoExtractor.getAudioSampleRate(audioPath) guard customSampleRate > 0 else { - print("โš ๏ธ Could not detect custom audio sample rate") + PluginLog.print("โš ๏ธ Could not detect custom audio sample rate") return true // Assume compatible if we can't detect } @@ -226,14 +226,14 @@ internal class AudioSequenceBuilder { if let videoSampleRate = await getVideoAudioSampleRate(clip.inputPath), videoSampleRate > 0 && videoSampleRate != customSampleRate { - print( + PluginLog.print( "โŒ Sample rate mismatch: custom audio (\(customSampleRate) Hz) vs video (\(videoSampleRate) Hz)" ) return false } } - print("โœ… Sample rates are compatible") + PluginLog.print("โœ… Sample rates are compatible") return true } diff --git a/macos/Classes/src/features/render/helpers/CompositionBuilder.swift b/macos/Classes/src/features/render/helpers/CompositionBuilder.swift index e9a7198..224909d 100644 --- a/macos/Classes/src/features/render/helpers/CompositionBuilder.swift +++ b/macos/Classes/src/features/render/helpers/CompositionBuilder.swift @@ -56,8 +56,8 @@ internal class CompositionBuilder { ) } - print("๐ŸŽฌ Creating composition with \(videoClips.count) video clips") - print("๐Ÿ”Š Audio enabled: \(enableAudio)") + PluginLog.print("๐ŸŽฌ Creating composition with \(videoClips.count) video clips") + PluginLog.print("๐Ÿ”Š Audio enabled: \(enableAudio)") let composition = AVMutableComposition() @@ -70,7 +70,7 @@ internal class CompositionBuilder { // Add custom audio tracks var customAudioTracks: [(track: AVMutableCompositionTrack, config: AudioTrackConfig)] = [] for trackConfig in audioTracks { - print("๐ŸŽต Adding audio track: \(trackConfig.path)") + PluginLog.print("๐ŸŽต Adding audio track: \(trackConfig.path)") let audioBuilder = AudioSequenceBuilder( audioPath: trackConfig.path, targetDuration: videoResult.totalDuration @@ -111,18 +111,18 @@ internal class CompositionBuilder { // This fixes issues on older macOS versions var instructions: [AVVideoCompositionInstructionProtocol] = [] - print("") - print("๐ŸŽจ ===== CREATING VIDEO INSTRUCTIONS =====") - print(" Total clips to process: \(videoResult.clipInstructions.count)") - print( + PluginLog.print("") + PluginLog.print("๐ŸŽจ ===== CREATING VIDEO INSTRUCTIONS =====") + PluginLog.print(" Total clips to process: \(videoResult.clipInstructions.count)") + PluginLog.print( " Target render size: \(videoResult.renderSize.width) x \(videoResult.renderSize.height)" ) - print("==========================================") - print("") + PluginLog.print("==========================================") + PluginLog.print("") for (index, clipInstruction) in videoResult.clipInstructions.enumerated() { - print("๐ŸŽฌ Processing instruction for clip \(index)") - print( + PluginLog.print("๐ŸŽฌ Processing instruction for clip \(index)") + PluginLog.print( " Time range: \(String(format: "%.2f", clipInstruction.timeRange.start.seconds))s - \(String(format: "%.2f", (clipInstruction.timeRange.start + clipInstruction.timeRange.duration).seconds))s" ) @@ -157,10 +157,10 @@ internal class CompositionBuilder { backgroundColor: CGColor(red: 0, green: 0, blue: 0, alpha: 1) ) - print( + PluginLog.print( " โš™๏ธ Layer instruction configured with transform (trackID: \(videoResult.videoTrack.trackID))" ) - print("") + PluginLog.print("") instructions.append(instruction) } @@ -171,7 +171,7 @@ internal class CompositionBuilder { renderSize: compositionRenderSize ) - print("โœ… Composition created successfully with \(videoClips.count) clips") + PluginLog.print("โœ… Composition created successfully with \(videoClips.count) clips") // Return the track ID for fallback on older macOS versions let sourceTrackID = videoResult.videoTrack.trackID @@ -204,7 +204,7 @@ internal class CompositionBuilder { } audioMixInputParameters.append(inputParameters) - print("๐Ÿ”Š Applied per-clip volume to original audio track") + PluginLog.print("๐Ÿ”Š Applied per-clip volume to original audio track") } // Apply volume to custom audio tracks @@ -212,7 +212,7 @@ internal class CompositionBuilder { let inputParameters = AVMutableAudioMixInputParameters(track: track) inputParameters.setVolume(config.volume, at: .zero) audioMixInputParameters.append(inputParameters) - print("๐Ÿ”Š Applied volume \(config.volume) to custom audio track: \(config.path)") + PluginLog.print("๐Ÿ”Š Applied volume \(config.volume) to custom audio track: \(config.path)") } let audioMix = AVMutableAudioMix() @@ -239,10 +239,10 @@ internal class CompositionBuilder { let videoWidth = abs(displaySize.width) let videoHeight = abs(displaySize.height) - print(" ๐Ÿ“ Transform calculation:") - print(" Natural size: \(naturalSize.width) x \(naturalSize.height)") - print(" Display size (after rotation): \(videoWidth) x \(videoHeight)") - print(" Target render size: \(renderSize.width) x \(renderSize.height)") + PluginLog.print(" ๐Ÿ“ Transform calculation:") + PluginLog.print(" Natural size: \(naturalSize.width) x \(naturalSize.height)") + PluginLog.print(" Display size (after rotation): \(videoWidth) x \(videoHeight)") + PluginLog.print(" Target render size: \(renderSize.width) x \(renderSize.height)") // Calculate scale to fill the render size (we want videos to be the same size) let scaleX = renderSize.width / videoWidth @@ -253,21 +253,21 @@ internal class CompositionBuilder { let scalePercentage = scale * 100 if willBeScaled { - print( + PluginLog.print( " ๐Ÿ” SCALING: \(String(format: "%.1f%%", scalePercentage)) (factor: \(String(format: "%.3f", scale)))" ) - print( + PluginLog.print( " Scale X: \(String(format: "%.3f", scaleX)) | Scale Y: \(String(format: "%.3f", scaleY))" ) } else { - print(" โœ“ No scaling needed (video already fits render size)") + PluginLog.print(" โœ“ No scaling needed (video already fits render size)") } // Calculate the scaled video dimensions let scaledWidth = videoWidth * scale let scaledHeight = videoHeight * scale - print( + PluginLog.print( " Final video size: \(String(format: "%.1f", scaledWidth)) x \(String(format: "%.1f", scaledHeight))" ) @@ -281,7 +281,7 @@ internal class CompositionBuilder { let angle = atan2(preferredTransform.b, preferredTransform.a) let degrees = angle * 180 / .pi - print(" Rotation: \(String(format: "%.1f", degrees))ยฐ") + PluginLog.print(" Rotation: \(String(format: "%.1f", degrees))ยฐ") // 2. Scale the video to fit the render size transform = transform.scaledBy(x: scale, y: scale) @@ -298,20 +298,20 @@ internal class CompositionBuilder { finalTranslateX = translateY finalTranslateY = translateX transform = transform.translatedBy(x: finalTranslateX, y: finalTranslateY) - print( + PluginLog.print( " Translation (rotated coords): x=\(String(format: "%.1f", finalTranslateX)), y=\(String(format: "%.1f", finalTranslateY))" ) } else { finalTranslateX = translateX finalTranslateY = translateY transform = transform.translatedBy(x: finalTranslateX, y: finalTranslateY) - print( + PluginLog.print( " Translation: x=\(String(format: "%.1f", finalTranslateX)), y=\(String(format: "%.1f", finalTranslateY))" ) } - print(" โœ… Transform applied for clip \(clipIndex)") - print("") + PluginLog.print(" โœ… Transform applied for clip \(clipIndex)") + PluginLog.print("") return transform } diff --git a/macos/Classes/src/features/render/helpers/MediaInfoExtractor.swift b/macos/Classes/src/features/render/helpers/MediaInfoExtractor.swift index 728fbac..dc7bd8b 100644 --- a/macos/Classes/src/features/render/helpers/MediaInfoExtractor.swift +++ b/macos/Classes/src/features/render/helpers/MediaInfoExtractor.swift @@ -16,7 +16,7 @@ internal class MediaInfoExtractor { static func getVideoDuration(_ videoPath: String) async -> Int64 { let url = URL(fileURLWithPath: videoPath) guard FileManager.default.fileExists(atPath: url.path) else { - print("โŒ Video file does not exist: \(videoPath)") + PluginLog.print("โŒ Video file does not exist: \(videoPath)") return 0 } @@ -36,7 +36,7 @@ internal class MediaInfoExtractor { return Int64(duration.seconds * 1_000_000) } catch { - print("โŒ Failed to get video duration for \(videoPath): \(error.localizedDescription)") + PluginLog.print("โŒ Failed to get video duration for \(videoPath): \(error.localizedDescription)") return 0 } } @@ -48,7 +48,7 @@ internal class MediaInfoExtractor { static func getAudioDuration(_ audioPath: String) async -> Int64 { let url = URL(fileURLWithPath: audioPath) guard FileManager.default.fileExists(atPath: url.path) else { - print("โŒ Audio file does not exist: \(audioPath)") + PluginLog.print("โŒ Audio file does not exist: \(audioPath)") return 0 } @@ -67,10 +67,10 @@ internal class MediaInfoExtractor { } let durationUs = Int64(duration.seconds * 1_000_000) - print("๐Ÿ” Audio duration: \(durationUs / 1000) ms") + PluginLog.print("๐Ÿ” Audio duration: \(durationUs / 1000) ms") return durationUs } catch { - print("โŒ Failed to get audio duration: \(error.localizedDescription)") + PluginLog.print("โŒ Failed to get audio duration: \(error.localizedDescription)") return 0 } } @@ -112,14 +112,14 @@ internal class MediaInfoExtractor { let formatDesc = description as! CMFormatDescription if let basicDesc = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc) { let channelCount = Int(basicDesc.pointee.mChannelsPerFrame) - print("๐Ÿ” File \(videoPath): \(channelCount) audio channels") + PluginLog.print("๐Ÿ” File \(videoPath): \(channelCount) audio channels") return channelCount } } return nil } catch { - print( + PluginLog.print( "โŒ Failed to detect audio channels for \(videoPath): \(error.localizedDescription)") return nil } @@ -160,14 +160,14 @@ internal class MediaInfoExtractor { let formatDesc = description as! CMFormatDescription if let basicDesc = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc) { let sampleRate = Int(basicDesc.pointee.mSampleRate) - print("๐Ÿ” Audio sample rate: \(sampleRate) Hz") + PluginLog.print("๐Ÿ” Audio sample rate: \(sampleRate) Hz") return sampleRate } } return 0 } catch { - print("โŒ Failed to detect audio sample rate: \(error.localizedDescription)") + PluginLog.print("โŒ Failed to detect audio sample rate: \(error.localizedDescription)") return 0 } } @@ -244,7 +244,7 @@ internal class MediaInfoExtractor { static func getVideoFormatInfo(_ videoPath: String) async -> VideoFormatInfo { let url = URL(fileURLWithPath: videoPath) guard FileManager.default.fileExists(atPath: url.path) else { - print("โŒ Video file does not exist: \(videoPath)") + PluginLog.print("โŒ Video file does not exist: \(videoPath)") return VideoFormatInfo(isHevc: false, bitDepth: 8, isHdr: false, profile: nil) } @@ -319,7 +319,7 @@ internal class MediaInfoExtractor { } } - print( + PluginLog.print( "๐Ÿ” Video format: path=\(videoPath), isHevc=\(isHevc), bitDepth=\(bitDepth), isHdr=\(isHdr), profile=\(profile ?? "unknown")" ) @@ -327,7 +327,7 @@ internal class MediaInfoExtractor { isHevc: isHevc, bitDepth: bitDepth, isHdr: isHdr, profile: profile) } catch { - print( + PluginLog.print( "โŒ Failed to get video format info for \(videoPath): \(error.localizedDescription)") return VideoFormatInfo(isHevc: false, bitDepth: 8, isHdr: false, profile: nil) } diff --git a/macos/Classes/src/features/render/helpers/VideoSequenceBuilder.swift b/macos/Classes/src/features/render/helpers/VideoSequenceBuilder.swift index 3dc4aab..8fdc973 100644 --- a/macos/Classes/src/features/render/helpers/VideoSequenceBuilder.swift +++ b/macos/Classes/src/features/render/helpers/VideoSequenceBuilder.swift @@ -38,7 +38,7 @@ internal class VideoSequenceBuilder { } let durationMs = Int(totalDuration.seconds * 1000) - print("๐Ÿ” Total video duration: \(durationMs) ms") + PluginLog.print("๐Ÿ” Total video duration: \(durationMs) ms") return totalDuration } @@ -77,8 +77,8 @@ internal class VideoSequenceBuilder { ) } - print("๐ŸŽฌ Building video sequence with \(videoClips.count) clips") - print("๐Ÿ”Š Audio enabled: \(enableAudio)") + PluginLog.print("๐ŸŽฌ Building video sequence with \(videoClips.count) clips") + PluginLog.print("๐Ÿ”Š Audio enabled: \(enableAudio)") var totalDuration = CMTime.zero var maxRenderSize = CGSize.zero @@ -108,17 +108,17 @@ internal class VideoSequenceBuilder { preferredTrackID: kCMPersistentTrackID_Invalid ) if sharedAudioTrack != nil { - print("๐Ÿ”Š Created SHARED audio track for all clips (will prevent empty segments)") + PluginLog.print("๐Ÿ”Š Created SHARED audio track for all clips (will prevent empty segments)") } } // Process each video clip for (index, clip) in videoClips.enumerated() { - print("๐Ÿ“น Processing clip \(index): \(clip.inputPath)") + PluginLog.print("๐Ÿ“น Processing clip \(index): \(clip.inputPath)") let url = URL(fileURLWithPath: clip.inputPath) guard FileManager.default.fileExists(atPath: url.path) else { - print("โŒ ERROR: Video file does not exist: \(clip.inputPath)") + PluginLog.print("โŒ ERROR: Video file does not exist: \(clip.inputPath)") throw NSError( domain: "VideoSequenceBuilder", code: 3, @@ -148,13 +148,13 @@ internal class VideoSequenceBuilder { // Log video properties let angle = atan2(preferredTransform.b, preferredTransform.a) let degrees = angle * 180 / .pi - print("๐Ÿ“น Clip \(index) properties:") - print(" - Natural size: \(naturalSize.width) x \(naturalSize.height)") - print( + PluginLog.print("๐Ÿ“น Clip \(index) properties:") + PluginLog.print(" - Natural size: \(naturalSize.width) x \(naturalSize.height)") + PluginLog.print( " - Rotation: \(degrees)ยฐ (transform: [\(preferredTransform.a), \(preferredTransform.b), \(preferredTransform.c), \(preferredTransform.d), \(preferredTransform.tx), \(preferredTransform.ty)])" ) - print(" - Display size: \(correctedSize.width) x \(correctedSize.height)") - print(" - Frame rate: \(nominalFrameRate) fps") + PluginLog.print(" - Display size: \(correctedSize.width) x \(correctedSize.height)") + PluginLog.print(" - Frame rate: \(nominalFrameRate) fps") // Update max render size if correctedSize.width > maxRenderSize.width @@ -162,7 +162,7 @@ internal class VideoSequenceBuilder { { let oldSize = maxRenderSize maxRenderSize = correctedSize - print( + PluginLog.print( " - โฌ†๏ธ Max render size updated: \(oldSize.width)x\(oldSize.height) โ†’ \(maxRenderSize.width)x\(maxRenderSize.height)" ) } @@ -197,13 +197,13 @@ internal class VideoSequenceBuilder { let audioTrack = try? await MediaInfoExtractor.loadAudioTrack(from: asset), let sharedAudioTrack = sharedAudioTrack { - print("๐Ÿ”Š Processing audio for clip \(index)...") - print(" โœ… Audio track loaded from asset") - print(" Track ID: \(audioTrack.trackID)") - print( + PluginLog.print("๐Ÿ”Š Processing audio for clip \(index)...") + PluginLog.print(" โœ… Audio track loaded from asset") + PluginLog.print(" Track ID: \(audioTrack.trackID)") + PluginLog.print( " Duration: \(String(format: "%.2f", audioTrack.timeRange.duration.seconds))s" ) - print(" Format: \(audioTrack.mediaType)") + PluginLog.print(" Format: \(audioTrack.mediaType)") do { try sharedAudioTrack.insertTimeRange( @@ -211,62 +211,62 @@ internal class VideoSequenceBuilder { of: audioTrack, at: totalDuration ) - print(" โœ… Audio inserted into SHARED track!") - print( + PluginLog.print(" โœ… Audio inserted into SHARED track!") + PluginLog.print( " Source time range: \(String(format: "%.2f", clipTimeRange.start.seconds))s - \(String(format: "%.2f", (clipTimeRange.start + clipTimeRange.duration).seconds))s" ) - print( + PluginLog.print( " Inserted at composition time: \(String(format: "%.2f", totalDuration.seconds))s" ) - print( + PluginLog.print( " Audio duration: \(String(format: "%.2f", clipTimeRange.duration.seconds))s" ) } catch { - print(" โŒ ERROR inserting audio: \(error.localizedDescription)") - print(" Error details: \(error)") + PluginLog.print(" โŒ ERROR inserting audio: \(error.localizedDescription)") + PluginLog.print(" Error details: \(error)") } } totalDuration = CMTimeAdd(totalDuration, clipDuration) - print("โœ… Clip \(index) added successfully") - print(" - Duration: \(String(format: "%.2f", clipDuration.seconds))s") - print( + PluginLog.print("โœ… Clip \(index) added successfully") + PluginLog.print(" - Duration: \(String(format: "%.2f", clipDuration.seconds))s") + PluginLog.print( " - Time range in composition: \(String(format: "%.2f", totalDuration.seconds - clipDuration.seconds))s - \(String(format: "%.2f", totalDuration.seconds))s" ) } - print("") - print("๐Ÿ“Š ===== VIDEO SEQUENCE SUMMARY =====") - print(" Total clips: \(videoClips.count)") - print(" Total duration: \(String(format: "%.2f", totalDuration.seconds))s") - print(" Max render size: \(maxRenderSize.width) x \(maxRenderSize.height)") - print(" Max frame rate: \(maxFrameRate) fps") - print(" Clip instructions: \(clipInstructions.count)") + PluginLog.print("") + PluginLog.print("๐Ÿ“Š ===== VIDEO SEQUENCE SUMMARY =====") + PluginLog.print(" Total clips: \(videoClips.count)") + PluginLog.print(" Total duration: \(String(format: "%.2f", totalDuration.seconds))s") + PluginLog.print(" Max render size: \(maxRenderSize.width) x \(maxRenderSize.height)") + PluginLog.print(" Max frame rate: \(maxFrameRate) fps") + PluginLog.print(" Clip instructions: \(clipInstructions.count)") // Handle shared audio track - add to result if it has segments, otherwise remove from composition if let audioTrack = sharedAudioTrack { if !audioTrack.segments.isEmpty { originalAudioTracks.append(audioTrack) - print( + PluginLog.print( " ๐Ÿ”Š AUDIO TRACKS: 1 (shared track with \(audioTrack.segments.count) segment(s))" ) for (segIdx, segment) in audioTrack.segments.enumerated() { let timeMapping = segment as AVCompositionTrackSegment - print( + PluginLog.print( " Segment \(segIdx): \(String(format: "%.2f", timeMapping.timeMapping.target.start.seconds))s - \(String(format: "%.2f", (timeMapping.timeMapping.target.start + timeMapping.timeMapping.target.duration).seconds))s (duration: \(String(format: "%.2f", timeMapping.timeMapping.target.duration.seconds))s)" ) } } else { // Remove empty audio track from composition to prevent export errors composition.removeTrack(audioTrack) - print(" ๐Ÿ”Š AUDIO TRACKS: 0 (shared track was empty and removed from composition)") + PluginLog.print(" ๐Ÿ”Š AUDIO TRACKS: 0 (shared track was empty and removed from composition)") } } else { - print(" ๐Ÿ”Š AUDIO TRACKS: 0 (no audio track created)") + PluginLog.print(" ๐Ÿ”Š AUDIO TRACKS: 0 (no audio track created)") } - print("=====================================") - print("") + PluginLog.print("=====================================") + PluginLog.print("") return VideoSequenceResult( videoTrack: compositionVideoTrack, diff --git a/macos/Classes/src/features/render/helpers/VideoTranscoder.swift b/macos/Classes/src/features/render/helpers/VideoTranscoder.swift index 8ceb0b2..644994b 100644 --- a/macos/Classes/src/features/render/helpers/VideoTranscoder.swift +++ b/macos/Classes/src/features/render/helpers/VideoTranscoder.swift @@ -33,7 +33,7 @@ internal class VideoTranscoder { let formatInfo = await MediaInfoExtractor.getVideoFormatInfo(videoPath) let needsTranscode = formatInfo.needsTranscodingForEffects() - print( + PluginLog.print( "๐Ÿ” Video transcoding check: path=\(videoPath), " + "isHevc=\(formatInfo.isHevc), bitDepth=\(formatInfo.bitDepth), " + "isHdr=\(formatInfo.isHdr), needsTranscoding=\(needsTranscode)") @@ -51,11 +51,11 @@ internal class VideoTranscoder { static func transcodeToH264(_ videoPath: String) async -> TranscodeResult { // Check if transcoding is needed guard await needsTranscoding(videoPath) else { - print("โœ… No transcoding needed for: \(videoPath)") + PluginLog.print("โœ… No transcoding needed for: \(videoPath)") return .notNeeded(originalPath: videoPath) } - print("๐ŸŽฌ Starting HEVC 10-bit HDR โ†’ H.264 8-bit SDR transcoding for: \(videoPath)") + PluginLog.print("๐ŸŽฌ Starting HEVC 10-bit HDR โ†’ H.264 8-bit SDR transcoding for: \(videoPath)") let inputURL = URL(fileURLWithPath: videoPath) let outputURL = FileManager.default.temporaryDirectory @@ -66,15 +66,15 @@ internal class VideoTranscoder { // Verify output let outputInfo = await MediaInfoExtractor.getVideoFormatInfo(outputURL.path) - print("โœ… Transcoding completed: \(outputURL.path)") - print( + PluginLog.print("โœ… Transcoding completed: \(outputURL.path)") + PluginLog.print( " Output: isHevc=\(outputInfo.isHevc), bitDepth=\(outputInfo.bitDepth), isHdr=\(outputInfo.isHdr)" ) return .success(outputPath: outputURL.path) } catch { - print("โŒ Transcoding failed: \(error.localizedDescription)") + PluginLog.print("โŒ Transcoding failed: \(error.localizedDescription)") try? FileManager.default.removeItem(at: outputURL) return .error(error) } @@ -94,7 +94,7 @@ internal class VideoTranscoder { case .notNeeded(let originalPath): result[inputPath] = originalPath case .error: - print("โš ๏ธ Transcoding failed for \(inputPath), using original") + PluginLog.print("โš ๏ธ Transcoding failed for \(inputPath), using original") result[inputPath] = inputPath } } @@ -110,9 +110,9 @@ internal class VideoTranscoder { if path.contains("transcoded_") { do { try FileManager.default.removeItem(atPath: path) - print("๐Ÿ—‘๏ธ Cleaned up transcoded file: \(path)") + PluginLog.print("๐Ÿ—‘๏ธ Cleaned up transcoded file: \(path)") } catch { - print("โš ๏ธ Failed to clean up \(path): \(error.localizedDescription)") + PluginLog.print("โš ๏ธ Failed to clean up \(path): \(error.localizedDescription)") } } } @@ -234,8 +234,8 @@ internal class VideoTranscoder { exportSession.videoComposition = videoComposition } - print("๐ŸŽฌ Transcoding with AVAssetExportSession...") - print(" Input size: \(naturalSize), Output size: \(renderSize)") + PluginLog.print("๐ŸŽฌ Transcoding with AVAssetExportSession...") + PluginLog.print(" Input size: \(naturalSize), Output size: \(renderSize)") // Export if #available(macOS 15.0, *) { @@ -250,7 +250,7 @@ internal class VideoTranscoder { } } - print("โœ… Transcoding completed successfully") + PluginLog.print("โœ… Transcoding completed successfully") } /// Calculates output size accounting for rotation. diff --git a/macos/Classes/src/features/thumbnail/ThumbnailGenerator.swift b/macos/Classes/src/features/thumbnail/ThumbnailGenerator.swift index 7969f8a..55c68a6 100644 --- a/macos/Classes/src/features/thumbnail/ThumbnailGenerator.swift +++ b/macos/Classes/src/features/thumbnail/ThumbnailGenerator.swift @@ -82,7 +82,7 @@ class ThumbnailGenerator { let key = requestedTime.seconds guard let index = timeIndexMap[key] else { - print("โš ๏ธ Unexpected time: \(Int(key * 1000)) ms") + PluginLog.print("โš ๏ธ Unexpected time: \(Int(key * 1000)) ms") return } @@ -98,12 +98,12 @@ class ThumbnailGenerator { resultData[index] = data let elapsed = Int((Date().timeIntervalSince1970 - start) * 1000) - print( + PluginLog.print( "[\(index)] โœ… \(Int(key * 1000)) ms in \(elapsed) ms (\(data.count) bytes)" ) } else { let message = error?.localizedDescription ?? "Unknown error" - print("[\(index)] โŒ Failed at \(Int(key * 1000)) ms: \(message)") + PluginLog.print("[\(index)] โŒ Failed at \(Int(key * 1000)) ms: \(message)") } completed += 1 @@ -192,7 +192,7 @@ class ThumbnailGenerator { case "jpeg", "jpg": return .jpeg default: - print("โš ๏ธ Format \(format) not supported, falling back to JPEG") + PluginLog.print("โš ๏ธ Format \(format) not supported, falling back to JPEG") return .jpeg } }() @@ -219,7 +219,7 @@ class ThumbnailGenerator { do { duration = try await asset.load(.duration) } catch { - print("โŒ Failed to load duration: \(error.localizedDescription)") + PluginLog.print("โŒ Failed to load duration: \(error.localizedDescription)") return [] } } else { diff --git a/macos/Classes/src/shared/logging/PluginLog.swift b/macos/Classes/src/shared/logging/PluginLog.swift new file mode 100644 index 0000000..4924a5c --- /dev/null +++ b/macos/Classes/src/shared/logging/PluginLog.swift @@ -0,0 +1,89 @@ +import Foundation + +enum PluginLogLevel: Int { + case verbose = 0 + case debug = 1 + case info = 2 + case warning = 3 + case error = 4 + case none = 5 + + static var `default`: PluginLogLevel { + #if DEBUG + return .debug + #else + return .warning + #endif + } + + static func from(methodValue: String) throws -> PluginLogLevel { + switch methodValue.lowercased() { + case "verbose": + return .verbose + case "debug": + return .debug + case "info": + return .info + case "warning", "warn": + return .warning + case "error": + return .error + case "none": + return .none + default: + throw PluginLogError.unsupportedLevel(methodValue) + } + } +} + +enum PluginLogError: LocalizedError { + case unsupportedLevel(String) + + var errorDescription: String? { + switch self { + case .unsupportedLevel(let level): + return "Unsupported native log level: \(level)" + } + } +} + +enum PluginLog { + private static var minimumLevel = PluginLogLevel.default + + static func setMinimumLevel(_ methodValue: String) throws { + minimumLevel = try .from(methodValue: methodValue) + } + + static func print(_ items: Any..., separator: String = " ", terminator: String = "\n") { + let message = items.map { String(describing: $0) }.joined(separator: separator) + log(message, terminator: terminator) + } + + private static func log(_ message: String, terminator: String) { + let level = inferredLevel(for: message) + guard shouldLog(level) else { return } + Swift.print(message, terminator: terminator) + } + + private static func shouldLog(_ level: PluginLogLevel) -> Bool { + level.rawValue >= minimumLevel.rawValue && minimumLevel != .none + } + + private static func inferredLevel(for message: String) -> PluginLogLevel { + if message.contains("โŒ") || message.localizedCaseInsensitiveContains("error") || + message.localizedCaseInsensitiveContains("failed") + { + return .error + } + + if message.contains("โš ๏ธ") || message.localizedCaseInsensitiveContains("warn") { + return .warning + } + + if message.contains("โ„น๏ธ") || message.localizedCaseInsensitiveContains("info") { + return .info + } + + return .debug + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index da8041d..150c6a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: pro_video_editor description: "A Flutter video editor: Seamlessly enhance your videos with user-friendly editing features." -version: 1.14.4 +version: 1.15.0 homepage: https://github.com/hm21/pro_video_editor/ repository: https://github.com/hm21/pro_video_editor/ documentation: https://github.com/hm21/pro_video_editor/