diff --git a/android/src/main/kotlin/me/albemala/native_video_player/NativeVideoPlayerViewController.kt b/android/src/main/kotlin/me/albemala/native_video_player/NativeVideoPlayerViewController.kt index 50a3b9c..c12e16b 100644 --- a/android/src/main/kotlin/me/albemala/native_video_player/NativeVideoPlayerViewController.kt +++ b/android/src/main/kotlin/me/albemala/native_video_player/NativeVideoPlayerViewController.kt @@ -1,6 +1,8 @@ package me.albemala.native_video_player import android.content.Context +import android.os.Handler +import android.os.Looper import android.view.SurfaceView import android.view.View import android.widget.RelativeLayout @@ -29,6 +31,10 @@ class NativeVideoPlayerViewController( private val view: SurfaceView private val relativeLayout: RelativeLayout + private val positionUpdateHandler = Handler(Looper.getMainLooper()) + private var positionUpdateRunnable: Runnable? = null + private var lastPosition = -1L + init { api.delegate = this player = ExoPlayer.Builder(context).build() @@ -60,8 +66,9 @@ class NativeVideoPlayerViewController( } override fun dispose() { - api.dispose() player.removeListener(this) + stopPositionUpdates() + api.dispose() player.release() } @@ -81,11 +88,11 @@ class NativeVideoPlayerViewController( override fun getVideoInfo(): VideoInfo { val videoSize = player.videoSize - return VideoInfo(videoSize.height, videoSize.width, player.duration.toInt() / 1000) + return VideoInfo(videoSize.height.toLong(), videoSize.width.toLong(), player.duration) } - override fun getPlaybackPosition(): Int { - return player.currentPosition.toInt() / 1000 + override fun getPlaybackPosition(): Long { + return player.currentPosition } override fun play() { @@ -104,8 +111,8 @@ class NativeVideoPlayerViewController( return player.isPlaying } - override fun seekTo(position: Int) { - player.seekTo(position.toLong() * 1000) + override fun seekTo(position: Long) { + player.seekTo(position) } override fun setPlaybackSpeed(speed: Double) { @@ -122,7 +129,8 @@ class NativeVideoPlayerViewController( override fun onPlaybackStateChanged(@Player.State state: Int) { if (state == Player.STATE_READY) { - return api.onPlaybackReady() + api.onPlaybackReady() + startPositionUpdates() } if (state == Player.STATE_ENDED) { @@ -137,4 +145,26 @@ class NativeVideoPlayerViewController( api.onError(error.cause as Throwable) } } + + private fun startPositionUpdates() { + stopPositionUpdates() + positionUpdateRunnable = object : Runnable { + override fun run() { + val position = player.currentPosition + if (lastPosition != position) { + lastPosition = position + api.onPlaybackPositionChanged(position) + } + positionUpdateHandler.postDelayed(this, 8L) + } + } + positionUpdateHandler.post(positionUpdateRunnable!!) + } + + private fun stopPositionUpdates() { + positionUpdateRunnable?.let { + positionUpdateHandler.removeCallbacks(it) + positionUpdateRunnable = null + } + } } \ No newline at end of file diff --git a/android/src/main/kotlin/me/albemala/native_video_player/platform_interface/NativeVideoPlayerApi.kt b/android/src/main/kotlin/me/albemala/native_video_player/platform_interface/NativeVideoPlayerApi.kt index f6cc128..47b289b 100644 --- a/android/src/main/kotlin/me/albemala/native_video_player/platform_interface/NativeVideoPlayerApi.kt +++ b/android/src/main/kotlin/me/albemala/native_video_player/platform_interface/NativeVideoPlayerApi.kt @@ -7,12 +7,12 @@ import io.flutter.plugin.common.MethodChannel interface NativeVideoPlayerApiDelegate { fun loadVideoSource(videoSource: VideoSource) fun getVideoInfo(): VideoInfo - fun getPlaybackPosition(): Int + fun getPlaybackPosition(): Long fun play() fun pause() fun stop() fun isPlaying(): Boolean - fun seekTo(position: Int) + fun seekTo(position: Long) fun setPlaybackSpeed(speed: Double) fun setVolume(volume: Double) fun setLoop(loop: Boolean) @@ -48,6 +48,10 @@ class NativeVideoPlayerApi( channel.invokeMethod("onPlaybackEnded", null) } + fun onPlaybackPositionChanged(position: Long) { + channel.invokeMethod("onPlaybackPositionChanged", position) + } + fun onError(error: Throwable) { channel.invokeMethod("onError", error.message) } @@ -85,7 +89,7 @@ class NativeVideoPlayerApi( result.success(playing) } "seekTo" -> { - val position = methodCall.arguments as? Int + val position = methodCall.arguments as? Long ?: return result.error(invalidArgumentsErrorCode, invalidArgumentsErrorMessage, null) delegate?.seekTo(position) result.success(null) diff --git a/android/src/main/kotlin/me/albemala/native_video_player/platform_interface/VideoInfo.kt b/android/src/main/kotlin/me/albemala/native_video_player/platform_interface/VideoInfo.kt index 6bb4bc5..a46340b 100644 --- a/android/src/main/kotlin/me/albemala/native_video_player/platform_interface/VideoInfo.kt +++ b/android/src/main/kotlin/me/albemala/native_video_player/platform_interface/VideoInfo.kt @@ -1,11 +1,11 @@ package me.albemala.native_video_player.platform_interface data class VideoInfo( - val height: Int, - val width: Int, - val duration: Int + val height: Long, + val width: Long, + val duration: Long ) { - fun toMap(): Map = mapOf( + fun toMap(): Map = mapOf( "height" to height, "width" to width, "duration" to duration diff --git a/ios/Classes/NativeVideoPlayerViewController.swift b/ios/Classes/NativeVideoPlayerViewController.swift index 4972b32..127321e 100644 --- a/ios/Classes/NativeVideoPlayerViewController.swift +++ b/ios/Classes/NativeVideoPlayerViewController.swift @@ -7,6 +7,8 @@ public class NativeVideoPlayerViewController: NSObject, FlutterPlatformView { private let player: AVPlayer private let playerView: NativeVideoPlayerView private var loop = false + private var lastPosition: Int64 = -1 + private var timeObserver: Any? init( messenger: FlutterBinaryMessenger, @@ -20,7 +22,7 @@ public class NativeVideoPlayerViewController: NSObject, FlutterPlatformView { player = AVPlayer() playerView = NativeVideoPlayerView(frame: frame, player: player) super.init() - + api.delegate = self player.addObserver(self, forKeyPath: "status", context: nil) @@ -34,18 +36,19 @@ public class NativeVideoPlayerViewController: NSObject, FlutterPlatformView { print("Failed to set playback audio session. Error: \(error)") } } - + deinit { player.removeObserver(self, forKeyPath: "status") removeOnVideoCompletedObserver() - + removePeriodicTimeObserver() + player.replaceCurrentItem(with: nil) } - + public func view() -> UIView { playerView } - + } extension NativeVideoPlayerViewController: NativeVideoPlayerApiDelegate { @@ -60,18 +63,19 @@ extension NativeVideoPlayerViewController: NativeVideoPlayerApiDelegate { return } let videoAsset = - isUrl - ? AVURLAsset(url: uri, options: ["AVURLAssetHTTPHeaderFieldsKey": videoSource.headers]) - : AVAsset(url: uri) + isUrl + ? AVURLAsset(url: uri, options: ["AVURLAssetHTTPHeaderFieldsKey": videoSource.headers]) + : AVAsset(url: uri) let playerItem = AVPlayerItem(asset: videoAsset) - + removeOnVideoCompletedObserver() player.replaceCurrentItem(with: playerItem) addOnVideoCompletedObserver() - + api.onPlaybackReady() + addPeriodicTimeObserver() } - + func getVideoInfo() -> VideoInfo { let videoInfo = VideoInfo( height: getVideoHeight(), @@ -80,18 +84,18 @@ extension NativeVideoPlayerViewController: NativeVideoPlayerApiDelegate { ) return videoInfo } - + func play() { if player.currentItem?.currentTime() == player.currentItem?.duration { player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) } player.play() } - + func pause() { player.pause() } - + func stop(completion: @escaping () -> Void) { player.pause() if #available(iOS 15, *) { @@ -102,61 +106,63 @@ extension NativeVideoPlayerViewController: NativeVideoPlayerApiDelegate { completion() } } - + func isPlaying() -> Bool { player.rate != 0 && player.error == nil } - - func seekTo(position: Int, completion: @escaping () -> Void) { + + func seekTo(position: Int64, completion: @escaping () -> Void) { player.seek( - to: CMTimeMakeWithSeconds(Float64(position), preferredTimescale: Int32(NSEC_PER_SEC)), + to: CMTime(value: position, timescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero ) { _ in completion() } } - - func getPlaybackPosition() -> Int { - let currentTime = player.currentItem?.currentTime() ?? CMTime.zero - return Int(currentTime.isValid ? currentTime.seconds : 0) + + func getPlaybackPosition() -> Int64 { + guard let currentItem = player.currentItem else { return 0 } + let currentTime = currentItem.currentTime() + return currentTime.isValid ? Int64(currentTime.seconds * 1000) : 0 } - + func setPlaybackSpeed(speed: Double) { player.rate = Float(speed) } - + func setVolume(volume: Double) { player.volume = Float(volume) } - + func setLoop(loop: Bool) { self.loop = loop } } extension NativeVideoPlayerViewController { - private func getVideoDuration() -> Int { - Int(player.currentItem?.asset.duration.seconds ?? 0) + private func getVideoDuration() -> Int64 { + guard let currentItem = player.currentItem else { return 0 } + return Int64(currentItem.asset.duration.seconds * 1000) } - + private func getVideoHeight() -> Int { if let videoTrack = getVideoTrack() { return Int(videoTrack.naturalSize.height) } return 0 } - + private func getVideoWidth() -> Int { if let videoTrack = getVideoTrack() { return Int(videoTrack.naturalSize.width) } return 0 } - + private func getVideoTrack() -> AVAssetTrack? { if let tracks = player.currentItem?.asset.tracks(withMediaType: .video), - let track = tracks.first + let track = tracks.first { return track } @@ -198,7 +204,7 @@ extension NativeVideoPlayerViewController { api.onPlaybackEnded() } } - + private func addOnVideoCompletedObserver() { NotificationCenter.default.addObserver( self, @@ -207,7 +213,7 @@ extension NativeVideoPlayerViewController { object: player.currentItem ) } - + private func removeOnVideoCompletedObserver() { NotificationCenter.default.removeObserver( self, @@ -215,4 +221,26 @@ extension NativeVideoPlayerViewController { object: player.currentItem ) } + + private func addPeriodicTimeObserver() { + removePeriodicTimeObserver() + timeObserver = player.addPeriodicTimeObserver( + forInterval: CMTime(seconds: 1.0/120.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), + queue: .main + ) { [weak self] time in + guard let self = self else { return } + let position = Int64(time.seconds * 1000) + if lastPosition != position { + lastPosition = position + self.api.onPlaybackPositionChanged(position: position) + } + } + } + + private func removePeriodicTimeObserver() { + if let observer = timeObserver { + player.removeTimeObserver(observer) + timeObserver = nil + } + } } diff --git a/ios/Classes/PlatformInterface/NativeVideoPlayerApi.swift b/ios/Classes/PlatformInterface/NativeVideoPlayerApi.swift index be0870e..8607cb2 100644 --- a/ios/Classes/PlatformInterface/NativeVideoPlayerApi.swift +++ b/ios/Classes/PlatformInterface/NativeVideoPlayerApi.swift @@ -1,12 +1,12 @@ protocol NativeVideoPlayerApiDelegate: AnyObject { func loadVideoSource(videoSource: VideoSource) func getVideoInfo() -> VideoInfo - func getPlaybackPosition() -> Int + func getPlaybackPosition() -> Int64 func play() func pause() func stop(completion: @escaping () -> Void) func isPlaying() -> Bool - func seekTo(position: Int, completion: @escaping () -> Void) + func seekTo(position: Int64, completion: @escaping () -> Void) func setPlaybackSpeed(speed: Double) func setVolume(volume: Double) func setLoop(loop: Bool) @@ -44,6 +44,10 @@ class NativeVideoPlayerApi { channel.invokeMethod("onPlaybackEnded", arguments: nil) } + func onPlaybackPositionChanged(position: Int64) { + channel.invokeMethod("onPlaybackPositionChanged", arguments: position) + } + func onError(_ error: Error) { channel.invokeMethod("onError", arguments: error.localizedDescription) } @@ -81,7 +85,7 @@ class NativeVideoPlayerApi { let playing = delegate?.isPlaying() result(playing) case "seekTo": - guard let position = call.arguments as? Int else { + guard let position = call.arguments as? Int64 else { result(invalidArgumentsFlutterError) return } diff --git a/ios/Classes/PlatformInterface/VideoInfo.swift b/ios/Classes/PlatformInterface/VideoInfo.swift index 1fe5a4c..21e2877 100644 --- a/ios/Classes/PlatformInterface/VideoInfo.swift +++ b/ios/Classes/PlatformInterface/VideoInfo.swift @@ -1,7 +1,7 @@ struct VideoInfo { let height: Int let width: Int - let duration: Int + let duration: Int64 func toMap() -> [String: Any] { [ diff --git a/lib/src/native_video_player_controller.dart b/lib/src/native_video_player_controller.dart index 4982ec3..925bda3 100644 --- a/lib/src/native_video_player_controller.dart +++ b/lib/src/native_video_player_controller.dart @@ -12,8 +12,6 @@ class NativeVideoPlayerController with ChangeNotifier { VideoSource? _videoSource; VideoInfo? _videoInfo; - Timer? _playbackPositionTimer; - PlaybackStatus get _playbackStatus => onPlaybackStatusChanged.value; int get _playbackPosition => onPlaybackPositionChanged.value; @@ -77,6 +75,7 @@ class NativeVideoPlayerController with ChangeNotifier { viewId: viewId, onPlaybackReady: _onPlaybackReady, onPlaybackEnded: _onPlaybackEnded, + onPlaybackPositionChanged: _onPlaybackPositionChanged, onError: _onError, ); } @@ -88,7 +87,7 @@ class NativeVideoPlayerController with ChangeNotifier { onPlaybackReady.notifyListeners(); } - Future _onPlaybackEnded() async { + void _onPlaybackEnded() { onPlaybackStatusChanged.value = PlaybackStatus.stopped; onPlaybackEnded.notifyListeners(); } @@ -101,7 +100,6 @@ class NativeVideoPlayerController with ChangeNotifier { @override @protected void dispose() { - _stopPlaybackPositionTimer(); _api.dispose(); super.dispose(); } @@ -120,7 +118,6 @@ class NativeVideoPlayerController with ChangeNotifier { /// NOTE: This method might throw an exception if the video cannot be played. Future play() async { await _api.play(); - _startPlaybackPositionTimer(); onPlaybackStatusChanged.value = PlaybackStatus.playing; await setPlaybackSpeed(_playbackSpeed); } @@ -131,7 +128,6 @@ class NativeVideoPlayerController with ChangeNotifier { /// NOTE: This method might throw an exception if the video cannot be paused. Future pause() async { await _api.pause(); - _stopPlaybackPositionTimer(); onPlaybackStatusChanged.value = PlaybackStatus.paused; } @@ -142,9 +138,7 @@ class NativeVideoPlayerController with ChangeNotifier { /// NOTE: This method might throw an exception if the video cannot be stopped. Future stop() async { await _api.stop(); - _stopPlaybackPositionTimer(); onPlaybackStatusChanged.value = PlaybackStatus.stopped; - await _onPlaybackPositionTimerChanged(null); } /// Returns true if the video is playing, or false if it's stopped or paused. @@ -156,14 +150,14 @@ class NativeVideoPlayerController with ChangeNotifier { } } - /// Moves the playback position to the given position in seconds. + /// Moves the playback position to the given position in milliseconds. /// /// NOTE: This method might throw an exception if the video cannot be seeked. - Future seekTo(int seconds) async { - var position = seconds; - if (seconds < 0) position = 0; + Future seekTo(int milliseconds) async { + var position = milliseconds; + if (milliseconds < 0) position = 0; final duration = videoInfo?.duration ?? 0; - if (seconds > duration) position = duration; + if (milliseconds > duration) position = duration; await _api.seekTo(position); // if the video is not playing, update onPlaybackPositionChanged if (_playbackStatus != PlaybackStatus.playing) { @@ -171,20 +165,20 @@ class NativeVideoPlayerController with ChangeNotifier { } } - /// Seeks the video forward by the given number of seconds. - Future seekForward(int seconds) async { + /// Seeks the video forward by the given number of milliseconds. + Future seekForward(int milliseconds) async { final duration = videoInfo?.duration ?? 0; - final newPlaybackPosition = _playbackPosition + seconds > duration // + final newPlaybackPosition = _playbackPosition + milliseconds > duration // ? duration - : _playbackPosition + seconds; + : _playbackPosition + milliseconds; await seekTo(newPlaybackPosition); } - /// Seeks the video backward by the given number of seconds. - Future seekBackward(int seconds) async { - final newPlaybackPosition = _playbackPosition - seconds < 0 // + /// Seeks the video backward by the given number of milliseconds. + Future seekBackward(int milliseconds) async { + final newPlaybackPosition = _playbackPosition - milliseconds < 0 // ? 0 - : _playbackPosition - seconds; + : _playbackPosition - milliseconds; await seekTo(newPlaybackPosition); } @@ -212,24 +206,8 @@ class NativeVideoPlayerController with ChangeNotifier { return _api.setLoop(loop); } - void _startPlaybackPositionTimer() { - _stopPlaybackPositionTimer(); - _playbackPositionTimer ??= Timer.periodic( - const Duration(milliseconds: 100), - _onPlaybackPositionTimerChanged, - ); - } - - void _stopPlaybackPositionTimer() { - if (_playbackPositionTimer == null) return; - _playbackPositionTimer!.cancel(); - _playbackPositionTimer = null; - } - - /// NOTE: This method can throw an exception - /// if the playback position cannot be retrieved. - Future _onPlaybackPositionTimerChanged(Timer? timer) async { - final position = await _api.getPlaybackPosition() ?? 0; + // ignore: use_setters_to_change_properties + void _onPlaybackPositionChanged(int position) { onPlaybackPositionChanged.value = position; } } diff --git a/lib/src/platform_interface/native_video_player_api.dart b/lib/src/platform_interface/native_video_player_api.dart index d4dc892..a8063ff 100644 --- a/lib/src/platform_interface/native_video_player_api.dart +++ b/lib/src/platform_interface/native_video_player_api.dart @@ -6,6 +6,7 @@ class NativeVideoPlayerApi { final int viewId; final void Function() onPlaybackReady; final void Function() onPlaybackEnded; + final void Function(int) onPlaybackPositionChanged; final void Function(String?) onError; late final MethodChannel _channel; @@ -13,6 +14,7 @@ class NativeVideoPlayerApi { required this.viewId, required this.onPlaybackReady, required this.onPlaybackEnded, + required this.onPlaybackPositionChanged, required this.onError, }) { final name = 'me.albemala.native_video_player.api.$viewId'; @@ -30,6 +32,9 @@ class NativeVideoPlayerApi { onPlaybackReady(); case 'onPlaybackEnded': onPlaybackEnded(); + case 'onPlaybackPositionChanged': + final position = call.arguments as int; + onPlaybackPositionChanged(position); case 'onError': // final errorCode = call.arguments['errorCode'] as int; // final errorMessage = call.arguments['errorMessage'] as String; diff --git a/lib/src/playback_info.dart b/lib/src/playback_info.dart index 0749c16..43cd0ae 100644 --- a/lib/src/playback_info.dart +++ b/lib/src/playback_info.dart @@ -6,7 +6,7 @@ class PlaybackInfo { /// The current playback status. final PlaybackStatus status; - /// The current playback position, in seconds. + /// The current playback position, in milliseconds. final int position; /// The current playback position as a value between 0 and 1. diff --git a/lib/src/video_info.dart b/lib/src/video_info.dart index d11eff1..8858076 100644 --- a/lib/src/video_info.dart +++ b/lib/src/video_info.dart @@ -12,7 +12,7 @@ class VideoInfo { /// Width of the video frame, in pixels. final int width; - /// Duration of the video, in seconds. + /// Duration of the video, in milliseconds. final int duration; /// Aspect ratio of the video frame, in pixels.