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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -60,8 +66,9 @@ class NativeVideoPlayerViewController(
}

override fun dispose() {
api.dispose()
player.removeListener(this)
stopPositionUpdates()
api.dispose()
player.release()
}

Expand All @@ -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() {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Int> = mapOf(
fun toMap(): Map<String, Long> = mapOf(
"height" to height,
"width" to width,
"duration" to duration
Expand Down
94 changes: 61 additions & 33 deletions ios/Classes/NativeVideoPlayerViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)

Expand All @@ -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 {
Expand All @@ -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(),
Expand All @@ -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, *) {
Expand All @@ -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
}
Expand Down Expand Up @@ -198,7 +204,7 @@ extension NativeVideoPlayerViewController {
api.onPlaybackEnded()
}
}

private func addOnVideoCompletedObserver() {
NotificationCenter.default.addObserver(
self,
Expand All @@ -207,12 +213,34 @@ extension NativeVideoPlayerViewController {
object: player.currentItem
)
}

private func removeOnVideoCompletedObserver() {
NotificationCenter.default.removeObserver(
self,
name: .AVPlayerItemDidPlayToEndTime,
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
}
}
}
10 changes: 7 additions & 3 deletions ios/Classes/PlatformInterface/NativeVideoPlayerApi.swift
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
Expand Down
Loading
Loading