react-native-video-trim 5.0.5 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,578 @@
1
+ //
2
+ // VideoTrimmerViewController.swift
3
+ // VideoTrim
4
+ //
5
+ // Created by Duc Trung Mai on 20/5/25.
6
+ //
7
+
8
+ import UIKit
9
+ import AVKit
10
+ import React
11
+
12
+ extension CMTime {
13
+ var displayString: String {
14
+ let offset = TimeInterval(seconds)
15
+ let numberOfNanosecondsFloat = (offset - TimeInterval(Int(offset))) * 100.0
16
+ let nanoseconds = Int(numberOfNanosecondsFloat)
17
+
18
+ let formatter = CMTime.dateFormatter
19
+ return String(format: "%@.%02d", formatter.string(from: offset) ?? "00:00", nanoseconds)
20
+ }
21
+
22
+ private static var dateFormatter: DateComponentsFormatter = {
23
+ let formatter = DateComponentsFormatter()
24
+ formatter.unitsStyle = .positional
25
+ formatter.zeroFormattingBehavior = .pad
26
+ formatter.allowedUnits = [.minute, .second]
27
+ return formatter
28
+ }()
29
+ }
30
+
31
+ @available(iOS 13.0, *)
32
+ class VideoTrimmerViewController: UIViewController {
33
+ var asset: AVAsset? {
34
+ didSet {
35
+ if let _ = asset {
36
+ setupVideoTrimmer()
37
+ setupPlayerController()
38
+ setupTimeObserver()
39
+ updateLabels()
40
+ }
41
+ }
42
+ }
43
+ private var maximumDuration: Int?
44
+ private var minimumDuration: Int?
45
+ private var cancelButtonText = "Cancel"
46
+ private var saveButtonText = "Save"
47
+ var cancelBtnClicked: (() -> Void)?
48
+ var saveBtnClicked: ((CMTimeRange) -> Void)?
49
+ private var enableHapticFeedback = true
50
+
51
+ private let playerController = AVPlayerViewController()
52
+ private var trimmer: VideoTrimmer!
53
+ private var timingStackView: UIStackView!
54
+ private var leadingTrimLabel: UILabel!
55
+ private var currentTimeLabel: UILabel!
56
+ private var trailingTrimLabel: UILabel!
57
+ private var btnStackView: UIStackView!
58
+ private var cancelBtn: UIButton!
59
+ private var playBtn: UIButton!
60
+ private let loadingIndicator = UIActivityIndicatorView()
61
+ private var saveBtn: UIButton!
62
+ private let playIcon = UIImage(systemName: "play.fill")
63
+ private let pauseIcon = UIImage(systemName: "pause.fill")
64
+ private let audioBannerView = UIImage(systemName: "airpodsmax")
65
+ private var player: AVPlayer! { playerController.player }
66
+ private var timeObserverToken: Any?
67
+ private var autoplay = false
68
+ private var jumpToPositionOnLoad: Double = 0;
69
+ private var headerText: String?
70
+ private var headerTextSize = 16
71
+ private var headerTextColor: Double?
72
+ private var headerView: UIView?
73
+
74
+ var isSeekInProgress: Bool = false // Marker
75
+ private var chaseTime = CMTime.zero
76
+ private var preferredFrameRate: Float = 23.98
77
+
78
+ public func onAssetFailToLoad() {
79
+ loadingIndicator.stopAnimating()
80
+ btnStackView.removeArrangedSubview(loadingIndicator)
81
+ loadingIndicator.removeFromSuperview()
82
+
83
+ let imageViewContainer = UIView()
84
+ let imageView = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill"))
85
+ imageView.tintColor = .systemYellow
86
+ imageView.translatesAutoresizingMaskIntoConstraints = false
87
+
88
+ imageViewContainer.addSubview(imageView)
89
+ NSLayoutConstraint.activate([
90
+ imageView.widthAnchor.constraint(equalToConstant: 36),
91
+ imageView.heightAnchor.constraint(equalToConstant: 36),
92
+ imageView.centerXAnchor.constraint(equalTo: imageViewContainer.centerXAnchor),
93
+ imageView.centerYAnchor.constraint(equalTo: imageViewContainer.centerYAnchor)
94
+ ])
95
+ imageViewContainer.alpha = 0
96
+
97
+ btnStackView.insertArrangedSubview(imageViewContainer, at: 1)
98
+
99
+ UIView.animate(withDuration: 0.25, animations: {
100
+ imageViewContainer.alpha = 1
101
+ })
102
+ }
103
+
104
+ // MARK: - Input
105
+ @objc private func didBeginTrimmingFromStart(_ sender: VideoTrimmer) {
106
+ handleBeforeProgressChange()
107
+ }
108
+
109
+ @objc private func leadingGrabberChanged(_ sender: VideoTrimmer) {
110
+ handleProgressChanged(time: trimmer.selectedRange.start)
111
+ }
112
+
113
+ @objc private func didEndTrimmingFromStart(_ sender: VideoTrimmer) {
114
+ handleTrimmingEnd(true)
115
+ }
116
+
117
+ @objc private func didBeginTrimmingFromEnd(_ sender: VideoTrimmer) {
118
+ handleBeforeProgressChange()
119
+ }
120
+
121
+ @objc private func trailingGrabberChanged(_ sender: VideoTrimmer) {
122
+ handleProgressChanged(time: trimmer.selectedRange.end)
123
+ }
124
+
125
+ @objc private func didEndTrimmingFromEnd(_ sender: VideoTrimmer) {
126
+ handleTrimmingEnd(false)
127
+ }
128
+
129
+ @objc private func didBeginScrubbing(_ sender: VideoTrimmer) {
130
+ handleBeforeProgressChange()
131
+ }
132
+
133
+ @objc private func didEndScrubbing(_ sender: VideoTrimmer) {
134
+ updateLabels()
135
+ }
136
+
137
+ @objc private func progressDidChanged(_ sender: VideoTrimmer) {
138
+ handleProgressChanged(time: trimmer.progress)
139
+ }
140
+
141
+ // MARK: - Private
142
+ private func updateLabels() {
143
+ leadingTrimLabel.text = trimmer.selectedRange.start.displayString
144
+ currentTimeLabel.text = trimmer.progress.displayString
145
+ trailingTrimLabel.text = trimmer.selectedRange.end.displayString
146
+ }
147
+
148
+ private func handleBeforeProgressChange() {
149
+ updateLabels()
150
+ player.pause()
151
+ setPlayBtnIcon()
152
+ }
153
+
154
+ private func handleProgressChanged(time: CMTime) {
155
+ updateLabels()
156
+ seek(to: time)
157
+ }
158
+
159
+ private func handleTrimmingEnd(_ start: Bool) {
160
+ self.trimmer.progress = start ? trimmer.selectedRange.start : trimmer.selectedRange.end
161
+ updateLabels()
162
+ seek(to: trimmer.progress)
163
+ }
164
+
165
+ // MARK: - UIViewController
166
+ override func viewDidLoad() {
167
+ super.viewDidLoad()
168
+
169
+ setupView()
170
+ setupButtons()
171
+ setupTimeLabels()
172
+ }
173
+
174
+ override func viewWillDisappear(_ animated: Bool) {
175
+ super.viewWillDisappear(animated)
176
+
177
+ // if asset has been initialized
178
+ guard let _ = asset else { return }
179
+ player.pause()
180
+
181
+ // Clean up the observer
182
+ player.removeObserver(self, forKeyPath: "status")
183
+
184
+ if let token = timeObserverToken {
185
+ player.removeTimeObserver(token)
186
+ timeObserverToken = nil
187
+ }
188
+ // Remove observer
189
+ NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
190
+
191
+ playerController.player = nil
192
+ playerController.dismiss(animated: false, completion: nil)
193
+ }
194
+
195
+ public func pausePlayer() {
196
+ player.pause()
197
+ setPlayBtnIcon()
198
+ }
199
+
200
+ @objc private func togglePlay(sender: UIButton) {
201
+ if player.timeControlStatus == .playing {
202
+ player.pause()
203
+ } else {
204
+ if CMTimeCompare(trimmer.progress, trimmer.selectedRange.end) != -1 {
205
+ trimmer.progress = trimmer.selectedRange.start
206
+ self.seek(to: trimmer.progress)
207
+ }
208
+
209
+ player.play()
210
+ }
211
+
212
+ setPlayBtnIcon()
213
+ }
214
+
215
+ @objc private func onSaveBtnClicked() {
216
+ saveBtnClicked?(trimmer.selectedRange)
217
+ }
218
+
219
+ @objc private func onCancelBtnClicked() {
220
+ cancelBtnClicked?()
221
+ }
222
+
223
+ // MARK: - Setup Methods
224
+ private func setupView() {
225
+ self.overrideUserInterfaceStyle = .dark
226
+ view.backgroundColor = .black // need to have this otherwise during animation the background of this VC is still white in white theme
227
+
228
+ if let headerText = headerText {
229
+ headerView = UIView()
230
+ headerView!.translatesAutoresizingMaskIntoConstraints = false
231
+ view.addSubview(headerView!)
232
+ let headerTextView = UITextView()
233
+ headerTextView.text = headerText
234
+ headerTextView.textAlignment = .center
235
+
236
+ headerTextView.textColor = RCTConvert.uiColor(headerTextColor)
237
+ // UIColor.color(fromHexNumber: headerTextColor as NSNumber?, defaultColor: .white)
238
+
239
+ headerTextView.font = UIFont.systemFont(ofSize: CGFloat(headerTextSize)) // Set font size here
240
+ headerTextView.translatesAutoresizingMaskIntoConstraints = false
241
+ headerView!.addSubview(headerTextView)
242
+
243
+ NSLayoutConstraint.activate([
244
+ // HeaderView constraints
245
+ headerView!.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
246
+ headerView!.leadingAnchor.constraint(equalTo: view.leadingAnchor),
247
+ headerView!.trailingAnchor.constraint(equalTo: view.trailingAnchor),
248
+ headerView!.heightAnchor.constraint(greaterThanOrEqualToConstant: 50),
249
+
250
+ // HeaderText constraints
251
+ headerTextView.topAnchor.constraint(equalTo: headerView!.topAnchor),
252
+ headerTextView.bottomAnchor.constraint(equalTo: headerView!.bottomAnchor),
253
+ headerTextView.leadingAnchor.constraint(equalTo: headerView!.leadingAnchor),
254
+ headerTextView.trailingAnchor.constraint(equalTo: headerView!.trailingAnchor),
255
+ ])
256
+
257
+ view.layoutIfNeeded() // layout after activate constraints, otherwise headerView height = screen height, which leads to playerViewController is missing at runtime
258
+ }
259
+ }
260
+
261
+ private func setupButtons() {
262
+ cancelBtn = UIButton.createButton(title: cancelButtonText, font: .systemFont(ofSize: 18), titleColor: .white, target: self, action: #selector(onCancelBtnClicked))
263
+ playBtn = UIButton.createButton(image: playIcon, tintColor: .white, target: self, action: #selector(togglePlay(sender:)))
264
+ playBtn.alpha = 0
265
+ playBtn.isEnabled = false
266
+
267
+ saveBtn = UIButton.createButton(title: saveButtonText, font: .systemFont(ofSize: 18), titleColor: .systemBlue, target: self, action: #selector(onSaveBtnClicked))
268
+ saveBtn.alpha = 0
269
+ saveBtn.isEnabled = false
270
+
271
+ btnStackView = UIStackView(arrangedSubviews: [cancelBtn, loadingIndicator, saveBtn])
272
+ btnStackView.axis = .horizontal
273
+ btnStackView.alignment = .center
274
+ btnStackView.distribution = .fillEqually
275
+ btnStackView.spacing = UIStackView.spacingUseSystem
276
+ btnStackView.translatesAutoresizingMaskIntoConstraints = false
277
+
278
+ view.addSubview(btnStackView)
279
+
280
+ NSLayoutConstraint.activate([
281
+ btnStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
282
+ btnStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
283
+ btnStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16)
284
+ ])
285
+
286
+ loadingIndicator.startAnimating()
287
+ }
288
+
289
+ private func setupTimeLabels() {
290
+ leadingTrimLabel = UILabel.createLabel(textAlignment: .left, textColor: .white)
291
+ leadingTrimLabel.text = "00:00.000"
292
+ currentTimeLabel = UILabel.createLabel(textAlignment: .center, textColor: .white)
293
+ currentTimeLabel.text = "00:00.000"
294
+ trailingTrimLabel = UILabel.createLabel(textAlignment: .right, textColor: .white)
295
+ trailingTrimLabel.text = "00:00.000"
296
+
297
+ timingStackView = UIStackView(arrangedSubviews: [leadingTrimLabel, currentTimeLabel, trailingTrimLabel])
298
+ timingStackView.axis = .horizontal
299
+ timingStackView.alignment = .fill
300
+ timingStackView.distribution = .fillEqually
301
+ timingStackView.spacing = UIStackView.spacingUseSystem
302
+ view.addSubview(timingStackView)
303
+ timingStackView.translatesAutoresizingMaskIntoConstraints = false
304
+ NSLayoutConstraint.activate([
305
+ timingStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
306
+ timingStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
307
+ timingStackView.bottomAnchor.constraint(equalTo: btnStackView.topAnchor, constant: -8)
308
+ ])
309
+ }
310
+
311
+ private func setupVideoTrimmer() {
312
+ trimmer = VideoTrimmer()
313
+ trimmer.asset = asset
314
+ trimmer.minimumDuration = CMTime(seconds: 1, preferredTimescale: 600)
315
+ trimmer.enableHapticFeedback = enableHapticFeedback
316
+
317
+ if let maxDuration = maximumDuration {
318
+ trimmer.maximumDuration = CMTime(seconds: max(1, Double(maxDuration)), preferredTimescale: 600)
319
+ if trimmer.maximumDuration > asset!.duration {
320
+ trimmer.maximumDuration = asset!.duration
321
+ }
322
+ trimmer.selectedRange = CMTimeRange(start: .zero, end: trimmer.maximumDuration)
323
+ }
324
+
325
+ if let minDuration = minimumDuration {
326
+ trimmer.minimumDuration = CMTime(seconds: max(1, Double(minDuration)), preferredTimescale: 600)
327
+ }
328
+
329
+ trimmer.addTarget(self, action: #selector(didBeginScrubbing(_:)), for: VideoTrimmer.didBeginScrubbing)
330
+ trimmer.addTarget(self, action: #selector(didEndScrubbing(_:)), for: VideoTrimmer.didEndScrubbing)
331
+ trimmer.addTarget(self, action: #selector(progressDidChanged(_:)), for: VideoTrimmer.progressChanged)
332
+
333
+ trimmer.addTarget(self, action: #selector(didBeginTrimmingFromStart(_:)), for: VideoTrimmer.didBeginTrimmingFromStart)
334
+ trimmer.addTarget(self, action: #selector(leadingGrabberChanged(_:)), for: VideoTrimmer.leadingGrabberChanged)
335
+ trimmer.addTarget(self, action: #selector(didEndTrimmingFromStart(_:)), for: VideoTrimmer.didEndTrimmingFromStart)
336
+
337
+ trimmer.addTarget(self, action: #selector(didBeginTrimmingFromEnd(_:)), for: VideoTrimmer.didBeginTrimmingFromEnd)
338
+ trimmer.addTarget(self, action: #selector(trailingGrabberChanged(_:)), for: VideoTrimmer.trailingGrabberChanged)
339
+ trimmer.addTarget(self, action: #selector(didEndTrimmingFromEnd(_:)), for: VideoTrimmer.didEndTrimmingFromEnd)
340
+ trimmer.alpha = 0
341
+ view.addSubview(trimmer)
342
+ trimmer.translatesAutoresizingMaskIntoConstraints = false
343
+ NSLayoutConstraint.activate([
344
+ trimmer.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
345
+ trimmer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
346
+ trimmer.bottomAnchor.constraint(equalTo: timingStackView.topAnchor, constant: -16),
347
+ trimmer.heightAnchor.constraint(equalToConstant: 50)
348
+ ])
349
+
350
+ UIView.animate(withDuration: 0.25, animations: {
351
+ self.trimmer.alpha = 1
352
+ })
353
+ }
354
+
355
+ private func setupPlayerController() {
356
+ playerController.showsPlaybackControls = false
357
+ if #available(iOS 16.0, *) {
358
+ playerController.allowsVideoFrameAnalysis = false
359
+ }
360
+ playerController.player = AVPlayer()
361
+ player.replaceCurrentItem(with: AVPlayerItem(asset: asset!))
362
+
363
+ // Add observer for player status
364
+ player.addObserver(self, forKeyPath: "status", options: [.new, .initial], context: nil)
365
+
366
+ try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
367
+ addChild(playerController)
368
+ view.addSubview(playerController.view)
369
+ playerController.view.translatesAutoresizingMaskIntoConstraints = false
370
+ NSLayoutConstraint.activate([
371
+ playerController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
372
+ playerController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
373
+ playerController.view.topAnchor.constraint(equalTo: headerView != nil ? headerView!.bottomAnchor : view.safeAreaLayoutGuide.topAnchor),
374
+ playerController.view.bottomAnchor.constraint(equalTo: trimmer.topAnchor, constant: -16)
375
+ ])
376
+
377
+ // Add observer for the end of playback
378
+ NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
379
+ }
380
+
381
+ @objc private func playerDidFinishPlaying(note: NSNotification) {
382
+ // Directly set the play icon
383
+ // the reason in at this time player.timeControlStatus == .playing still returns true
384
+ playBtn.setImage(self.playIcon, for: .normal)
385
+ }
386
+
387
+ private func setupTimeObserver() {
388
+ timeObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 30), queue: .main) { [weak self] time in
389
+ guard let self = self else { return }
390
+
391
+ if self.player.timeControlStatus != .playing {
392
+ return
393
+ }
394
+
395
+ self.trimmer.progress = time
396
+
397
+ // pause if reach end of selected range
398
+ if CMTimeCompare(self.trimmer.progress, trimmer.selectedRange.end) == 1 {
399
+ player.pause()
400
+ self.trimmer.progress = trimmer.selectedRange.end
401
+ self.seek(to: trimmer.selectedRange.end)
402
+ }
403
+
404
+ currentTimeLabel.text = trimmer.progress.displayString
405
+
406
+ self.setPlayBtnIcon()
407
+ }
408
+ }
409
+
410
+ private func setPlayBtnIcon() {
411
+ self.playBtn.setImage(self.player.timeControlStatus == .playing ? self.pauseIcon : self.playIcon, for: .normal)
412
+ }
413
+
414
+ // ====Smoother seek
415
+ public func seek(to time: CMTime) {
416
+ seekSmoothlyToTime(newChaseTime: time)
417
+ }
418
+
419
+ private func seekSmoothlyToTime(newChaseTime: CMTime) {
420
+ if CMTimeCompare(newChaseTime, chaseTime) != 0 {
421
+ chaseTime = newChaseTime
422
+
423
+ if !isSeekInProgress {
424
+ trySeekToChaseTime()
425
+ }
426
+ }
427
+ }
428
+
429
+ private func trySeekToChaseTime() {
430
+ guard player?.status == .readyToPlay else { return }
431
+ actuallySeekToTime()
432
+ }
433
+
434
+ private func actuallySeekToTime() {
435
+ isSeekInProgress = true
436
+ let seekTimeInProgress = chaseTime
437
+
438
+ player?.seek(to: seekTimeInProgress, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] _ in
439
+ guard let `self` = self else { return }
440
+
441
+ if CMTimeCompare(seekTimeInProgress, self.chaseTime) == 0 {
442
+ self.isSeekInProgress = false
443
+ } else {
444
+ self.trySeekToChaseTime()
445
+ }
446
+ }
447
+ }
448
+
449
+ public func configure(config: NSDictionary) {
450
+ if let maxDuration = config["maxDuration"] as? Int, maxDuration > 0 {
451
+ maximumDuration = maxDuration
452
+ }
453
+
454
+ if let minDuration = config["minDuration"] as? Int, minDuration > 0 {
455
+ minimumDuration = minDuration
456
+ }
457
+
458
+ if let cancelText = config["cancelButtonText"] as? String, !cancelText.isEmpty {
459
+ cancelButtonText = cancelText
460
+ }
461
+
462
+ if let saveText = config["saveButtonText"] as? String, !saveText.isEmpty {
463
+ saveButtonText = saveText
464
+ }
465
+
466
+ if let jumpPosition = config["jumpToPositionOnLoad"] as? Double, jumpPosition >= 0 {
467
+ jumpToPositionOnLoad = jumpPosition
468
+ }
469
+
470
+ if let enableHaptic = config["enableHapticFeedback"] as? Bool {
471
+ enableHapticFeedback = enableHaptic
472
+ }
473
+
474
+ if let autoPlay = config["autoplay"] as? Bool {
475
+ autoplay = autoPlay
476
+ }
477
+
478
+ if let headerText = config["headerText"] as? String, !headerText.isEmpty {
479
+ self.headerText = headerText
480
+
481
+ if let textSize = config["headerTextSize"] as? Int, textSize > 0 {
482
+ headerTextSize = textSize
483
+ }
484
+
485
+ if let textColor = config["headerTextColor"] as? Double {
486
+ headerTextColor = textColor
487
+ }
488
+ }
489
+ }
490
+
491
+ override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
492
+ if keyPath == "status" {
493
+ if player.status == .readyToPlay {
494
+ loadingIndicator.stopAnimating()
495
+ btnStackView.removeArrangedSubview(loadingIndicator)
496
+ loadingIndicator.removeFromSuperview()
497
+ btnStackView.insertArrangedSubview(playBtn, at: 1)
498
+
499
+ UIView.animate(withDuration: 0.25, animations: {
500
+ self.playBtn.alpha = 1
501
+ self.playBtn.isEnabled = true
502
+ self.saveBtn.alpha = 1
503
+ self.saveBtn.isEnabled = true
504
+ })
505
+
506
+ if jumpToPositionOnLoad > 0 {
507
+ let duration = (asset?.duration.seconds ?? 0) * 1000
508
+ let time = jumpToPositionOnLoad > duration ? duration : jumpToPositionOnLoad
509
+ let cmtime = CMTime(value: CMTimeValue(time), timescale: 1000)
510
+
511
+ self.seek(to: cmtime)
512
+ self.trimmer.progress = cmtime
513
+ self.currentTimeLabel.text = self.trimmer.progress.displayString
514
+ }
515
+
516
+ if autoplay {
517
+ togglePlay(sender: playBtn)
518
+ }
519
+ }
520
+ }
521
+ }
522
+ }
523
+
524
+ private extension UIButton {
525
+ static func createButton(title: String? = nil, image: UIImage? = nil, font: UIFont? = nil, titleColor: UIColor? = nil, tintColor: UIColor? = nil, target: Any?, action: Selector) -> UIButton {
526
+ let button = UIButton(type: .system)
527
+ if let title = title {
528
+ button.setTitle(title, for: .normal)
529
+ }
530
+ if let image = image {
531
+ button.setImage(image, for: .normal)
532
+ }
533
+ if let font = font {
534
+ button.titleLabel?.font = font
535
+ }
536
+ if let titleColor = titleColor {
537
+ button.setTitleColor(titleColor, for: .normal)
538
+ }
539
+ if let tintColor = tintColor {
540
+ button.tintColor = tintColor
541
+ }
542
+ button.addTarget(target, action: action, for: .touchUpInside)
543
+ return button
544
+ }
545
+ }
546
+
547
+ private extension UILabel {
548
+ static func createLabel(textAlignment: NSTextAlignment, textColor: UIColor) -> UILabel {
549
+ let label = UILabel()
550
+ label.font = UIFont.preferredFont(forTextStyle: .caption1)
551
+ label.textAlignment = textAlignment
552
+ label.textColor = textColor
553
+ return label
554
+ }
555
+ }
556
+
557
+ //extension UIColor {
558
+ // static func color(fromHexNumber hex: NSNumber?, defaultColor: UIColor = .black) -> UIColor {
559
+ // guard let hexValue = hex?.int32Value else {
560
+ // return defaultColor
561
+ // }
562
+ //
563
+ // // Extract RGB components from the hex value
564
+ // let red = CGFloat((hexValue >> 16) & 0xFF) / 255.0 // Extract red (bits 16-23)
565
+ // let green = CGFloat((hexValue >> 8) & 0xFF) / 255.0 // Extract green (bits 8-15)
566
+ // let blue = CGFloat(hexValue & 0xFF) / 255.0 // Extract blue (bits 0-7)
567
+ //
568
+ // // Check if alpha is included (if hex is 0xAARRGGBB)
569
+ // let alpha: CGFloat
570
+ // if hexValue > 0xFFFFFF { // If the value is larger than 0xFFFFFF, it includes alpha
571
+ // alpha = CGFloat((hexValue >> 24) & 0xFF) / 255.0 // Extract alpha (bits 24-31)
572
+ // } else {
573
+ // alpha = 1.0 // Default to opaque
574
+ // }
575
+ //
576
+ // return UIColor(red: red, green: green, blue: blue, alpha: alpha)
577
+ // }
578
+ //}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-video-trim",
3
- "version": "5.0.5",
3
+ "version": "5.1.0",
4
4
  "description": "Video trimmer for your React Native app",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
package/ios/AssetLoader.h DELETED
@@ -1,19 +0,0 @@
1
- #import <Foundation/Foundation.h>
2
- #import <AVFoundation/AVFoundation.h>
3
-
4
- @class AssetLoader;
5
-
6
- @protocol AssetLoaderDelegate <NSObject>
7
- @optional
8
- - (void)assetLoaderDidSucceed:(AssetLoader *)assetLoader;
9
- - (void)assetLoader:(AssetLoader *)assetLoader didFailWithError:(NSError *)error forKey:(NSString *)key;
10
- @end
11
-
12
- @interface AssetLoader : NSObject
13
-
14
- @property (nonatomic, weak) id<AssetLoaderDelegate> delegate;
15
- @property (nonatomic, strong, readonly) AVURLAsset *asset;
16
-
17
- - (void)loadAssetWithURL:(NSURL *)url isVideoType:(BOOL)isVideoType;
18
-
19
- @end
@@ -1,87 +0,0 @@
1
- #import "AssetLoader.h"
2
-
3
- @interface AssetLoader ()
4
- @property (nonatomic, strong) AVURLAsset *asset;
5
- @end
6
-
7
- @implementation AssetLoader
8
-
9
- - (void)loadAssetWithURL:(NSURL *)url isVideoType:(BOOL)isVideoType {
10
- NSDictionary *options = @{ AVURLAssetPreferPreciseDurationAndTimingKey: @YES };
11
- self.asset = [AVURLAsset URLAssetWithURL:url options:options];
12
- NSArray *keys = @[ @"duration", @"tracks" ];
13
- [self.asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
14
- [self assetLoaded:isVideoType];
15
- }];
16
- }
17
-
18
- - (void)assetLoaded:(BOOL)isVideoType {
19
- NSArray *keys = @[ @"duration", @"tracks" ];
20
- for (NSString *key in keys) {
21
- NSError *error = nil;
22
- AVKeyValueStatus status = [self.asset statusOfValueForKey:key error:&error];
23
- if (status == AVKeyValueStatusFailed) {
24
- if ([self.delegate respondsToSelector:@selector(assetLoader:didFailWithError:forKey:)]) {
25
- [self.delegate assetLoader:self didFailWithError:error forKey:key];
26
- }
27
- return;
28
- } else if (status == AVKeyValueStatusCancelled) {
29
- NSError *cancelError = [NSError errorWithDomain:@"AssetLoader" code:-1 userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ loading was cancelled", key] }];
30
- if ([self.delegate respondsToSelector:@selector(assetLoader:didFailWithError:forKey:)]) {
31
- [self.delegate assetLoader:self didFailWithError:cancelError forKey:key];
32
- }
33
- return;
34
- } else if (status != AVKeyValueStatusLoaded) {
35
- NSError *unknownError = [NSError errorWithDomain:@"AssetLoader" code:-1 userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is in an unknown state", key] }];
36
- if ([self.delegate respondsToSelector:@selector(assetLoader:didFailWithError:forKey:)]) {
37
- [self.delegate assetLoader:self didFailWithError:unknownError forKey:key];
38
- }
39
- return;
40
- }
41
- }
42
- if (isVideoType) {
43
- [self processAssetTracks];
44
- } else {
45
- if ([self.delegate respondsToSelector:@selector(assetLoaderDidSucceed:)]) {
46
- [self.delegate assetLoaderDidSucceed:self];
47
- }
48
- }
49
- }
50
-
51
- - (void)processAssetTracks {
52
- NSArray *videoTracks = [self.asset tracksWithMediaType:AVMediaTypeVideo];
53
- AVAssetTrack *videoTrack = videoTracks.firstObject;
54
- if (!videoTrack) {
55
- NSError *error = [NSError errorWithDomain:@"AssetLoader" code:-1 userInfo:@{ NSLocalizedDescriptionKey: @"No video tracks found" }];
56
- if ([self.delegate respondsToSelector:@selector(assetLoader:didFailWithError:forKey:)]) {
57
- [self.delegate assetLoader:self didFailWithError:error forKey:@"tracks"];
58
- }
59
- return;
60
- }
61
- NSArray *trackKeys = @[ @"naturalSize", @"preferredTransform" ];
62
- [videoTrack loadValuesAsynchronouslyForKeys:trackKeys completionHandler:^{
63
- [self trackPropertiesLoaded:videoTrack];
64
- }];
65
- }
66
-
67
- - (void)trackPropertiesLoaded:(AVAssetTrack *)track {
68
- NSError *error = nil;
69
- AVKeyValueStatus naturalSizeStatus = [track statusOfValueForKey:@"naturalSize" error:&error];
70
- AVKeyValueStatus preferredTransformStatus = [track statusOfValueForKey:@"preferredTransform" error:&error];
71
- if (naturalSizeStatus == AVKeyValueStatusLoaded && preferredTransformStatus == AVKeyValueStatusLoaded) {
72
- CGSize naturalSize = track.naturalSize;
73
- CGAffineTransform preferredTransform = track.preferredTransform;
74
- NSLog(@"Natural size: %@", NSStringFromCGSize(naturalSize));
75
- NSLog(@"Preferred transform: %@", NSStringFromCGAffineTransform(preferredTransform));
76
- if ([self.delegate respondsToSelector:@selector(assetLoaderDidSucceed:)]) {
77
- [self.delegate assetLoaderDidSucceed:self];
78
- }
79
- } else {
80
- if ([self.delegate respondsToSelector:@selector(assetLoader:didFailWithError:forKey:)]) {
81
- NSString *failedKey = (naturalSizeStatus != AVKeyValueStatusLoaded) ? @"naturalSize" : @"preferredTransform";
82
- [self.delegate assetLoader:self didFailWithError:error forKey:failedKey];
83
- }
84
- }
85
- }
86
-
87
- @end