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,10 +1,14 @@
package me.albemala.native_video_player

import androidx.annotation.NonNull
import androidx.media3.datasource.DataSource

import io.flutter.embedding.engine.plugins.FlutterPlugin

class NativeVideoPlayerPlugin : FlutterPlugin {
companion object {
var dataSourceFactory: ((Map<String, String>) -> DataSource.Factory)? = null
}

override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
val factory = NativeVideoPlayerViewFactory(binding.binaryMessenger)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource
Expand All @@ -23,6 +24,7 @@ class NativeVideoPlayerViewController(
viewId: Int,
context: Context,
private val api: NativeVideoPlayerApi = NativeVideoPlayerApi(messenger, viewId),
private val dataSourceFactory: ((Map<String, String>) -> DataSource.Factory)? = null,
) : PlatformView,
NativeVideoPlayerApiDelegate,
Player.Listener {
Expand Down Expand Up @@ -78,7 +80,8 @@ class NativeVideoPlayerViewController(
when (videoSource.type) {
VideoSourceType.Asset, VideoSourceType.File -> player.setMediaItem(mediaItem)
VideoSourceType.Network -> {
val dataSource = DefaultHttpDataSource.Factory().setDefaultRequestProperties(videoSource.headers)
val dataSource = dataSourceFactory?.invoke(videoSource.headers)
?: DefaultHttpDataSource.Factory().setDefaultRequestProperties(videoSource.headers)
val mediaSource = ProgressiveMediaSource.Factory(dataSource).createMediaSource(mediaItem)
player.setMediaSource(mediaSource)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class NativeVideoPlayerViewFactory(
return NativeVideoPlayerViewController(
messenger,
viewId,
context
context,
dataSourceFactory = NativeVideoPlayerPlugin.dataSourceFactory,
)
}
}
157 changes: 89 additions & 68 deletions ios/Classes/NativeVideoPlayerViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ public class NativeVideoPlayerViewController: NSObject, FlutterPlatformView {
private var loop = false
private var lastPosition: Int64 = -1
private var timeObserver: Any?
private var timeControlObserver: NSKeyValueObservation?

init(
messenger: FlutterBinaryMessenger,
viewId: Int64,
frame: CGRect
frame: CGRect,
) {
api = NativeVideoPlayerApi(
messenger: messenger,
Expand All @@ -28,9 +29,7 @@ public class NativeVideoPlayerViewController: NSObject, FlutterPlatformView {

// Play audio even when the device is in silent mode
do {
try AVAudioSession.sharedInstance().setCategory(
.playback,
mode: .default)
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Failed to set playback audio session. Error: \(error)")
Expand All @@ -39,9 +38,10 @@ public class NativeVideoPlayerViewController: NSObject, FlutterPlatformView {

deinit {
player.removeObserver(self, forKeyPath: "status")
timeControlObserver?.invalidate()
removeOnVideoCompletedObserver()
removePeriodicTimeObserver()

player.replaceCurrentItem(with: nil)
}

Expand All @@ -55,34 +55,87 @@ extension NativeVideoPlayerViewController: NativeVideoPlayerApiDelegate {
func loadVideoSource(videoSource: VideoSource) {
let isUrl = videoSource.type == .network
let sourcePath = videoSource.path
guard
let uri = isUrl
? URL(string: sourcePath)
: URL(fileURLWithPath: sourcePath)
else {
return
guard let uri = isUrl ? URL(string: sourcePath) : URL(fileURLWithPath: sourcePath) else { return }

let videoAsset: AVAsset
if isUrl, #available(iOS 15.0, *), let proxyURL = VideoProxyServer.shared.proxyURL(for: uri) {
videoAsset = AVURLAsset(url: proxyURL)
} else if isUrl {
videoAsset = AVURLAsset(url: uri, options: ["AVURLAssetHTTPHeaderFieldsKey": videoSource.headers])
} else {
videoAsset = AVAsset(url: uri)
}
let videoAsset =
isUrl
? AVURLAsset(url: uri, options: ["AVURLAssetHTTPHeaderFieldsKey": videoSource.headers])
: AVAsset(url: uri)

let playerItem = AVPlayerItem(asset: videoAsset)

removeOnVideoCompletedObserver()
player.replaceCurrentItem(with: playerItem)
addOnVideoCompletedObserver()

timeControlObserver = addTimeControlObserver(currentItem: playerItem)
api.onPlaybackReady()
addPeriodicTimeObserver()
}

func getVideoInfo(completion: @escaping (VideoInfo) -> Void) {
if #available(iOS 15, *) {
getVideoInfoAsync(completion: completion)
} else {
getVideoInfoLegacy(completion: completion)
}
}

@available(iOS 15, *)
private func getVideoInfoAsync(completion: @escaping (VideoInfo) -> Void) {
guard let asset = player.currentItem?.asset else {
return completion(VideoInfo(height: 0, width: 0, duration: 0))
}
Task {
do {
async let d = asset.load(.duration)
async let t = asset.loadTracks(withMediaType: .video)

let duration = try await d
let size = try await t.first?.load(.naturalSize) ?? .zero

let info = VideoInfo(
height: Int(size.height),
width: Int(size.width),
duration: Int64(duration.seconds * 1000)
)
completion(info)
} catch {
completion(VideoInfo(height: 0, width: 0, duration: 0))
}
}
}

func getVideoInfo() -> VideoInfo {
let videoInfo = VideoInfo(
height: getVideoHeight(),
width: getVideoWidth(),
duration: getVideoDuration()
)
return videoInfo
private func getVideoInfoLegacy(completion: @escaping (VideoInfo) -> Void) {
guard let asset = player.currentItem?.asset else {
return completion(VideoInfo(height: 0, width: 0, duration: 0))
}

asset.loadValuesAsynchronously(forKeys: ["duration", "tracks"]) {
let duration: Int64
if asset.statusOfValue(forKey: "duration", error: nil) == .loaded {
duration = Int64(asset.duration.seconds * 1000)
} else {
duration = 0
}

var width = 0, height = 0
if asset.statusOfValue(forKey: "tracks", error: nil) == .loaded,
let track = asset.tracks(withMediaType: .video).first {
track.loadValuesAsynchronously(forKeys: ["naturalSize"]) {
if track.statusOfValue(forKey: "naturalSize", error: nil) == .loaded {
width = Int(track.naturalSize.width)
height = Int(track.naturalSize.height)
}
completion(VideoInfo(height: height, width: width, duration: duration))
}
return
}

completion(VideoInfo(height: height, width: width, duration: duration))
}
}

func play() {
Expand Down Expand Up @@ -140,57 +193,15 @@ extension NativeVideoPlayerViewController: NativeVideoPlayerApiDelegate {
}
}

extension NativeVideoPlayerViewController {
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
{
return track
}
return nil
}
}

extension NativeVideoPlayerViewController {
override public func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?
) {
if keyPath == "status" {
switch player.status {
case .unknown:
break
case .readyToPlay:
break
case .failed:
if let error = player.error {
api.onError(error)
}
default:
break
}
}
guard keyPath == "status", player.status == .failed, let error = player.error else { return }
api.onError(error)
}
}

Expand Down Expand Up @@ -222,6 +233,16 @@ extension NativeVideoPlayerViewController {
)
}

private func addTimeControlObserver(currentItem: AVPlayerItem) -> NSKeyValueObservation {
return currentItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { [weak self] item, _ in
if item.isPlaybackLikelyToKeepUp,
self?.player.timeControlStatus == .waitingToPlayAtSpecifiedRate {
// AVPlayer can sometimes get stuck
DispatchQueue.main.async { [weak self] in self?.player.play() }
}
}
}

private func addPeriodicTimeObserver() {
removePeriodicTimeObserver()
timeObserver = player.addPeriodicTimeObserver(
Expand Down
5 changes: 2 additions & 3 deletions ios/Classes/PlatformInterface/NativeVideoPlayerApi.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
protocol NativeVideoPlayerApiDelegate: AnyObject {
func loadVideoSource(videoSource: VideoSource)
func getVideoInfo() -> VideoInfo
func getVideoInfo(completion: @escaping (VideoInfo) -> Void)
func getPlaybackPosition() -> Int64
func play()
func pause()
Expand Down Expand Up @@ -66,8 +66,7 @@ class NativeVideoPlayerApi {
delegate?.loadVideoSource(videoSource: videoSource)
result(nil)
case "getVideoInfo":
let videoInfo = delegate?.getVideoInfo().toMap()
result(videoInfo)
delegate?.getVideoInfo { info in result(info.toMap()) }
case "getPlaybackPosition":
let playbackPosition = delegate?.getPlaybackPosition()
result(playbackPosition)
Expand Down
Loading
Loading