This commit is contained in:
Fredrik Burmester
2024-10-11 22:10:47 +02:00
parent 57354e6b06
commit be867a3b10
8 changed files with 773 additions and 512 deletions

View File

@@ -1,22 +1,88 @@
import ExpoModulesCore
public class VlcPlayerModule: Module {
// Each module class must implement the definition function. The definition consists of components
// that describes the module's functionality and behavior.
// See https://docs.expo.dev/modules/module-api for more details about available components.
public func definition() -> ModuleDefinition {
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
// The module will be accessible from `requireNativeModule('VlcPlayer')` in JavaScript.
Name("VlcPlayer")
View(VlcPlayerView.self) {
Prop("source") { (view: VlcPlayerView, source: String) in
view.setSource(source)
}
}
public func definition() -> ModuleDefinition {
Name("VlcPlayer")
View(VlcPlayerView.self) {
Prop("source") { (view: VlcPlayerView, source: [String: Any]) in
view.setSource(source)
}
Function("hello") {
return "hello from native ios"
Prop("progressUpdateInterval") { (view: VlcPlayerView, interval: Double) in
view.setProgressUpdateInterval(interval)
}
Prop("paused") { (view: VlcPlayerView, paused: Bool) in
if paused {
view.pause()
} else {
view.play()
}
}
Prop("muted") { (view: VlcPlayerView, muted: Bool) in
view.setMuted(muted)
}
Prop("volume") { (view: VlcPlayerView, volume: Int) in
view.setVolume(volume)
}
Prop("videoAspectRatio") { (view: VlcPlayerView, ratio: String) in
view.setVideoAspectRatio(ratio)
}
Events(
"onProgress",
"onPlaybackStateChanged",
"onVideoLoadStart",
"onVideoStateChange",
"onVideoProgress"
)
AsyncFunction("play") { (view: VlcPlayerView) in
view.play()
}
AsyncFunction("pause") { (view: VlcPlayerView) in
view.pause()
}
AsyncFunction("seekTo") { (view: VlcPlayerView, time: Double) in
view.seekTo(time)
}
AsyncFunction("jumpBackward") { (view: VlcPlayerView, interval: Int) in
view.jumpBackward(interval)
}
AsyncFunction("jumpForward") { (view: VlcPlayerView, interval: Int) in
view.jumpForward(interval)
}
AsyncFunction("setAudioTrack") { (view: VlcPlayerView, trackIndex: Int) in
view.setAudioTrack(trackIndex)
}
AsyncFunction("getAudioTracks") { (view: VlcPlayerView) -> [[String: Any]]? in
return view.getAudioTracks()
}
AsyncFunction("setSubtitleTrack") { (view: VlcPlayerView, trackIndex: Int) in
view.setSubtitleTrack(trackIndex)
}
AsyncFunction("getSubtitleTracks") { (view: VlcPlayerView) -> [[String: Any]]? in
return view.getSubtitleTracks()
}
AsyncFunction("setVideoCropGeometry") { (view: VlcPlayerView, geometry: String?) in
view.setVideoCropGeometry(geometry)
}
AsyncFunction("getVideoCropGeometry") { (view: VlcPlayerView) -> String? in
return view.getVideoCropGeometry()
}
}
}
}
}

View File

@@ -1,77 +1,413 @@
import ExpoModulesCore
import UIKit
import MobileVLCKit
import UIKit
class VlcPlayerView: ExpoView, VLCMediaPlayerDelegate {
private var mediaPlayer: VLCMediaPlayer?
private var movieView: UIView?
class VlcPlayerView: ExpoView {
private var mediaPlayer: VLCMediaPlayer?
private var videoView: UIView?
private var progressUpdateTimer: Timer?
private var progressUpdateInterval: TimeInterval = 0.5
private var isPaused: Bool = false
private var currentGeometryCString: [CChar]?
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
DispatchQueue.main.async {
self.setupView()
self.backgroundColor = UIColor.black // Set background color to black
// MARK: - Initialization
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
setupView()
setupNotifications()
}
}
private func setupView() {
DispatchQueue.main.async {
self.movieView = UIView()
self.movieView?.translatesAutoresizingMaskIntoConstraints = false
// MARK: - Setup
if let movieView = self.movieView {
self.addSubview(movieView)
NSLayoutConstraint.activate([
movieView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
movieView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
movieView.topAnchor.constraint(equalTo: self.topAnchor),
movieView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
}
private func setupView() {
DispatchQueue.main.async {
self.backgroundColor = .black
self.videoView = UIView()
self.videoView?.translatesAutoresizingMaskIntoConstraints = false
self.setupMediaPlayer()
if let videoView = self.videoView {
self.addSubview(videoView)
NSLayoutConstraint.activate([
videoView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
videoView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
videoView.topAnchor.constraint(equalTo: self.topAnchor),
videoView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
])
}
self.setupMediaPlayer()
}
}
}
private func setupMediaPlayer() {
DispatchQueue.main.async {
self.mediaPlayer = VLCMediaPlayer()
self.mediaPlayer?.delegate = self
self.mediaPlayer?.drawable = self.movieView
print("Media player setup on main thread: \(Thread.isMainThread)")
private func setupMediaPlayer() {
DispatchQueue.main.async {
self.mediaPlayer = VLCMediaPlayer()
self.mediaPlayer?.delegate = self
self.mediaPlayer?.drawable = self.videoView
}
}
}
@objc func setSource(_ source: String) {
DispatchQueue.main.async {
print("Setting media source on main thread: \(Thread.isMainThread)")
if let url = URL(string: source) {
self.mediaPlayer?.media = VLCMedia(url: url)
print("Media set, now playing...")
private func setupNotifications() {
NotificationCenter.default.addObserver(
self, selector: #selector(applicationWillResignActive),
name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(
self, selector: #selector(applicationWillEnterForeground),
name: UIApplication.willEnterForegroundNotification, object: nil)
}
// MARK: - Public Methods
@objc func play() {
self.mediaPlayer?.play()
} else {
print("Invalid URL.")
}
self.isPaused = false
}
}
@objc func handlePlayPause() {
DispatchQueue.main.async {
print("Handling play/pause on main thread: \(Thread.isMainThread)")
if self.mediaPlayer?.isPlaying == true {
@objc func pause() {
self.mediaPlayer?.pause()
} else {
self.mediaPlayer?.play()
}
self.isPaused = true
}
}
func mediaPlayerStateChanged(_ aNotification: Notification!) {
DispatchQueue.main.async {
print("Media player state changed on main thread: \(Thread.isMainThread)")
if self.mediaPlayer?.state == .stopped {
print("Media player stopped")
}
@objc func seekTo(_ time: Double) {
self.mediaPlayer?.time = VLCTime(int: Int32(time * 1000))
}
}
}
@objc func setSource(_ source: [String: Any]) {
DispatchQueue.main.async {
self.mediaPlayer?.stop()
self.mediaPlayer = nil
let mediaOptions = source["mediaOptions"] as? [String: Any]
let initOptions = source["initOptions"] as? [Any]
let uri = source["uri"] as? String
let initType = source["initType"] as? Int ?? 0
let autoplay = source["autoplay"] as? Bool ?? false
let isNetwork = source["isNetwork"] as? Bool ?? false
guard let uri = uri, !uri.isEmpty else { return }
if initType == 2, let options = initOptions {
self.mediaPlayer = VLCMediaPlayer(options: options)
} else {
self.mediaPlayer = VLCMediaPlayer()
}
self.mediaPlayer?.delegate = self
self.mediaPlayer?.drawable = self.videoView
self.mediaPlayer?.scaleFactor = 0
let media: VLCMedia
if isNetwork {
media = VLCMedia(url: URL(string: uri)!)
} else {
media = VLCMedia(path: uri)
}
media.delegate = self
if let mediaOptions = mediaOptions {
media.addOptions(mediaOptions)
}
// Parse the media asynchronously
media.parse()
self.mediaPlayer?.media = media
if autoplay {
self.play()
}
self.onVideoLoadStart?(["target": self.reactTag ?? NSNull()])
}
}
@objc func setProgressUpdateInterval(_ interval: Double) {
progressUpdateInterval = TimeInterval(interval / 1000.0)
updateProgressTimer()
}
@objc func jumpBackward(_ interval: Int) {
mediaPlayer?.jumpBackward(Int32(interval))
}
@objc func jumpForward(_ interval: Int) {
mediaPlayer?.jumpForward(Int32(interval))
}
@objc func setMuted(_ muted: Bool) {
mediaPlayer?.audio?.isMuted = muted
}
@objc func setVolume(_ volume: Int) {
mediaPlayer?.audio?.volume = Int32(volume)
}
@objc func setVideoAspectRatio(_ ratio: String) {
DispatchQueue.main.async {
ratio.withCString { cString in
self.mediaPlayer?.videoAspectRatio = UnsafeMutablePointer(mutating: cString)
}
}
}
@objc func setAudioTrack(_ trackIndex: Int) {
DispatchQueue.main.async {
self.mediaPlayer?.currentAudioTrackIndex = Int32(trackIndex)
}
}
@objc func getAudioTracks() -> [[String: Any]]? {
guard let trackNames = mediaPlayer?.audioTrackNames,
let trackIndexes = mediaPlayer?.audioTrackIndexes
else {
return nil
}
return zip(trackNames, trackIndexes).map { name, index in
return ["name": name, "index": index]
}
}
@objc func setSubtitleTrack(_ trackIndex: Int) {
DispatchQueue.main.async {
self.mediaPlayer?.currentVideoSubTitleIndex = Int32(trackIndex)
}
}
@objc func getSubtitleTracks() -> [[String: Any]]? {
guard let trackNames = mediaPlayer?.videoSubTitlesNames,
let trackIndexes = mediaPlayer?.videoSubTitlesIndexes
else {
return nil
}
return zip(trackNames, trackIndexes).map { name, index in
return ["name": name, "index": index]
}
}
@objc func setSubtitleDelay(_ delay: Int) {
DispatchQueue.main.async {
self.mediaPlayer?.currentVideoSubTitleDelay = NSInteger(delay)
}
}
@objc func setAudioDelay(_ delay: Int) {
DispatchQueue.main.async {
self.mediaPlayer?.currentAudioPlaybackDelay = NSInteger(delay)
}
}
@objc func takeSnapshot(_ path: String, width: Int, height: Int) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.mediaPlayer?.saveVideoSnapshot(
at: path, withWidth: Int32(width), andHeight: Int32(height))
}
}
@objc func setVideoCropGeometry(_ geometry: String?) {
DispatchQueue.main.async {
if let geometry = geometry, !geometry.isEmpty {
self.currentGeometryCString = geometry.cString(using: .utf8)
self.currentGeometryCString?.withUnsafeMutableBufferPointer { buffer in
self.mediaPlayer?.videoCropGeometry = buffer.baseAddress
}
} else {
self.currentGeometryCString = nil
self.mediaPlayer?.videoCropGeometry = nil
}
}
}
@objc func getVideoCropGeometry() -> String? {
guard let cString = mediaPlayer?.videoCropGeometry else {
return nil
}
return String(cString: cString)
}
@objc func setRate(_ rate: Float) {
DispatchQueue.main.async {
self.mediaPlayer?.rate = rate
}
}
@objc func nextChapter() {
DispatchQueue.main.async {
self.mediaPlayer?.nextChapter()
}
}
@objc func previousChapter() {
DispatchQueue.main.async {
self.mediaPlayer?.previousChapter()
}
}
@objc func getChapters() -> [[String: Any]]? {
guard let currentTitleIndex = mediaPlayer?.currentTitleIndex,
let chapters = mediaPlayer?.chapterDescriptions(ofTitle: currentTitleIndex)
as? [[String: Any]]
else {
return nil
}
return chapters.compactMap { chapter in
guard let name = chapter[VLCChapterDescriptionName] as? String,
let timeOffset = chapter[VLCChapterDescriptionTimeOffset] as? NSNumber,
let duration = chapter[VLCChapterDescriptionDuration] as? NSNumber
else {
return nil
}
return [
"name": name,
"timeOffset": timeOffset.doubleValue,
"duration": duration.doubleValue,
]
}
}
// MARK: - Private Methods
private func updateProgressTimer() {
progressUpdateTimer?.invalidate()
progressUpdateTimer = Timer.scheduledTimer(
withTimeInterval: progressUpdateInterval, repeats: true
) { [weak self] _ in
self?.sendProgressUpdate()
}
}
private func sendProgressUpdate() {
DispatchQueue.main.async {
guard let player = self.mediaPlayer else { return }
let currentTime = player.time.intValue
let duration = player.media?.length.intValue ?? 0
let progress: [String: Any] = [
"currentTime": currentTime,
"duration": duration,
]
self.onVideoProgress?(progress)
}
}
@objc private func applicationWillResignActive() {
if !isPaused {
pause()
}
}
@objc private func applicationWillEnterForeground() {
if !isPaused {
play()
}
}
private func release() {
DispatchQueue.main.async {
self.mediaPlayer?.stop()
self.mediaPlayer = nil
NotificationCenter.default.removeObserver(self)
}
}
// MARK: - Expo Events
@objc var onProgress: RCTDirectEventBlock?
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
@objc var onVideoLoadStart: RCTDirectEventBlock?
@objc var onVideoStateChange: RCTDirectEventBlock?
@objc var onVideoProgress: RCTDirectEventBlock?
// MARK: - Deinitialization
deinit {
release()
}
}
extension VlcPlayerView: VLCMediaPlayerDelegate {
func mediaPlayerStateChanged(_ aNotification: Notification) {
DispatchQueue.main.async {
guard let player = self.mediaPlayer else { return }
let state = player.state
var stateInfo: [String: Any] = [
"target": self.reactTag ?? NSNull(),
"currentTime": player.time.intValue,
"duration": player.media?.length.intValue ?? 0,
]
switch state {
case .opening:
stateInfo["type"] = "Opening"
case .paused:
self.isPaused = true
stateInfo["type"] = "Paused"
case .stopped:
stateInfo["type"] = "Stopped"
case .buffering:
if player.isPlaying {
// If the player is actually playing while in buffering state,
// we'll report it as "Playing"
self.isPaused = false
stateInfo["type"] = "Playing"
} else {
stateInfo["type"] = "Buffering"
stateInfo["isBuffering"] = true
}
case .playing:
self.isPaused = false
stateInfo["type"] = "Playing"
case .esAdded:
stateInfo["type"] = "ESAdded"
case .ended:
print("VLCMediaPlayerStateEnded")
stateInfo["type"] = "Ended"
case .error:
stateInfo["type"] = "Error"
self.release()
@unknown default:
stateInfo["type"] = "Unknown"
}
self.onVideoStateChange?(stateInfo)
}
}
func mediaPlayerTimeChanged(_ aNotification: Notification) {
updateVideoProgress()
}
private func updateVideoProgress() {
DispatchQueue.main.async {
guard let player = self.mediaPlayer else { return }
let currentTime = player.time.intValue
let duration = player.media?.length.intValue ?? 0
if currentTime >= 0 && currentTime < duration {
self.onVideoProgress?([
"target": self.reactTag ?? NSNull(),
"currentTime": currentTime,
"duration": duration,
])
}
}
}
}
extension VlcPlayerView: VLCMediaDelegate {
func mediaMetaDataDidChange(_ aMedia: VLCMedia) {
// Implement if needed
}
func mediaDidFinishParsing(_ aMedia: VLCMedia) {
DispatchQueue.main.async {
let duration = aMedia.length.intValue
self.onVideoStateChange?(["type": "MediaParsed", "duration": duration])
}
}
}