shakebug-react-native 2.4.9 → 2.4.10
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.
- package/README.md +0 -12
- package/VideoPlayer/PlayVideo.tsx +4 -46
- package/VideoPlayer/index.js +6 -43
- package/android/src/main/java/com/shakebug/BaseUtils.java +7 -19
- package/android/src/main/java/com/shakebug/DeviceUtil.java +34 -21
- package/android/src/main/java/com/shakebug/ShakebugPackage.java +3 -3
- package/android/src/main/java/com/shakebug/UnitConverter.java +8 -8
- package/android/src/main/java/com/shakebug/VideoPlayerModule.java +81 -0
- package/android/src/main/java/com/shakebug/VideoTrimModule.java +387 -0
- package/android/src/main/java/com/shakebug/VideoTrimUtil.java +133 -0
- package/android/src/main/java/com/shakebug/adapters/VideoTrimmerAdapter.java +53 -0
- package/android/src/main/java/com/shakebug/callback/SingleCallback.java +0 -6
- package/android/src/main/java/com/shakebug/thread/BackgroundExecutor.java +1 -93
- package/android/src/main/java/com/shakebug/thread/UiThreadExecutor.java +0 -37
- package/android/src/main/java/com/shakebug/utils/StorageUtil.java +135 -0
- package/android/src/main/java/com/shakebug/utils/VideoTrimmerUtil.java +70 -0
- package/android/src/main/java/com/shakebug/widgets/RangeSeekBarView.java +534 -0
- package/android/src/main/java/com/shakebug/widgets/SpacesItemDecoration2.java +1 -2
- package/android/src/main/java/com/shakebug/widgets/VideoPlayerView.java +317 -0
- package/android/src/main/java/com/shakebug/widgets/VideoTrimmerView.java +520 -0
- package/android/src/main/res/drawable/ic_volume_off.xml +10 -0
- package/android/src/main/res/drawable/ic_volume_on.xml +10 -0
- package/android/src/main/res/layout/{video_player_view.xml → shakebug_video_player_view.xml} +29 -9
- package/ios/TrimVideo/ShakebugSDKVideoTrim.swift +389 -0
- package/ios/TrimVideo/VideoTrim-Bridging-Header.h +10 -0
- package/ios/TrimVideo/VideoTrim.m +23 -0
- package/ios/TrimVideo/VideoTrimmer.swift +810 -0
- package/ios/TrimVideo/VideoTrimmerThumb.swift +119 -0
- package/ios/TrimVideo/VideoTrimmerViewController.swift +322 -0
- package/ios/VideoPlayer/VideoPlayerModule.m +23 -7
- package/package.json +1 -1
- package/src/Componanats/LoadingModal.js +59 -0
- package/src/Componanats/ShakeFunction.js +2 -10
- package/src/Screens/AddDetailsScreen.js +28 -31
- package/src/Screens/FirstScreen.js +15 -18
- package/src/Screens/Shakebug.js +1 -1
- package/src/Screens/TrimVideoScreen.js +37 -22
- package/src/index.tsx +4 -4
- package/trimVideo/src/index.tsx +12 -48
- package/shakebug-react-native-2.4.8.tgz +0 -0
- /package/android/src/main/res/layout/{video_thumb_item_layout.xml → shakebug_video_thumb_item_layout.xml} +0 -0
- /package/android/src/main/res/layout/{video_trimmer_view.xml → shakebug_video_trimmer_view.xml} +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//
|
|
2
|
+
// VideoTrimmerThumb.swift
|
|
3
|
+
// Shakebug
|
|
4
|
+
//
|
|
5
|
+
// Created by ideveloper2 on 26/04/24.
|
|
6
|
+
// Copyright © 2024 Facebook. All rights reserved.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import UIKit
|
|
10
|
+
|
|
11
|
+
@available(iOS 13.0, *)
|
|
12
|
+
class VideoTrimmerThumb: UIView {
|
|
13
|
+
var isActive = false
|
|
14
|
+
|
|
15
|
+
var leadingChevronImageView = UIImageView(image: UIImage(systemName: "chevron.compact.left"))
|
|
16
|
+
var trailingChevronView = UIImageView(image: UIImage(systemName: "chevron.compact.right"))
|
|
17
|
+
|
|
18
|
+
var wrapperView = UIView()
|
|
19
|
+
var leadingView = UIView()
|
|
20
|
+
var trailingView = UIView()
|
|
21
|
+
var topView = UIView()
|
|
22
|
+
var bottomView = UIView()
|
|
23
|
+
|
|
24
|
+
let leadingGrabber = UIControl()
|
|
25
|
+
let trailingGrabber = UIControl()
|
|
26
|
+
|
|
27
|
+
let chevronWidth = CGFloat(16)
|
|
28
|
+
let edgeHeight = CGFloat(4)
|
|
29
|
+
|
|
30
|
+
// MARK: - Input
|
|
31
|
+
@objc private func x(_ sender: Any) {
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// MARK: - Private
|
|
36
|
+
private func updateColor() {
|
|
37
|
+
let color = UIColor.systemYellow
|
|
38
|
+
leadingView.backgroundColor = color
|
|
39
|
+
trailingView.backgroundColor = color
|
|
40
|
+
topView.backgroundColor = color
|
|
41
|
+
bottomView.backgroundColor = color
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private func setup() {
|
|
45
|
+
|
|
46
|
+
leadingChevronImageView.contentMode = .scaleAspectFill
|
|
47
|
+
trailingChevronView.contentMode = .scaleAspectFill
|
|
48
|
+
|
|
49
|
+
leadingChevronImageView.tintColor = .white
|
|
50
|
+
trailingChevronView.tintColor = .white
|
|
51
|
+
|
|
52
|
+
leadingChevronImageView.tintAdjustmentMode = .normal
|
|
53
|
+
trailingChevronView.tintAdjustmentMode = .normal
|
|
54
|
+
|
|
55
|
+
leadingView.layer.cornerRadius = 6
|
|
56
|
+
leadingView.layer.cornerCurve = .continuous
|
|
57
|
+
leadingView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMinXMinYCorner]
|
|
58
|
+
|
|
59
|
+
trailingView.layer.cornerRadius = 6
|
|
60
|
+
trailingView.layer.cornerCurve = .continuous
|
|
61
|
+
trailingView.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMaxXMinYCorner]
|
|
62
|
+
|
|
63
|
+
leadingView.addSubview(leadingChevronImageView)
|
|
64
|
+
trailingView.addSubview(trailingChevronView)
|
|
65
|
+
|
|
66
|
+
// wrapperView.layer.shadowColor = UIColor.black.cgColor
|
|
67
|
+
// wrapperView.layer.shadowOffset = .zero
|
|
68
|
+
// wrapperView.layer.shadowRadius = 2
|
|
69
|
+
// wrapperView.layer.shadowOpacity = 0.25
|
|
70
|
+
|
|
71
|
+
wrapperView.addSubview(leadingView)
|
|
72
|
+
wrapperView.addSubview(trailingView)
|
|
73
|
+
wrapperView.addSubview(topView)
|
|
74
|
+
wrapperView.addSubview(bottomView)
|
|
75
|
+
addSubview(wrapperView)
|
|
76
|
+
|
|
77
|
+
wrapperView.addSubview(leadingGrabber)
|
|
78
|
+
wrapperView.addSubview(trailingGrabber)
|
|
79
|
+
|
|
80
|
+
updateColor()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
// MARK: - UIView
|
|
86
|
+
|
|
87
|
+
override func layoutSubviews() {
|
|
88
|
+
super.layoutSubviews()
|
|
89
|
+
|
|
90
|
+
let size = bounds.size
|
|
91
|
+
|
|
92
|
+
wrapperView.frame = CGRect(origin: .zero, size: size)
|
|
93
|
+
|
|
94
|
+
leadingView.frame = CGRect(x: 0, y: 0, width: chevronWidth, height: bounds.height)
|
|
95
|
+
trailingView.frame = CGRect(x: bounds.width - chevronWidth, y: 0, width: chevronWidth, height: bounds.height)
|
|
96
|
+
topView.frame = CGRect(x: chevronWidth, y: 0, width: bounds.width - chevronWidth * 2, height: edgeHeight)
|
|
97
|
+
bottomView.frame = CGRect(x: chevronWidth, y: bounds.height - edgeHeight, width: bounds.width - chevronWidth * 2, height: edgeHeight)
|
|
98
|
+
|
|
99
|
+
let chevronHorizontalInset = CGFloat(2)
|
|
100
|
+
let chevronVerticalInset = CGFloat(8)
|
|
101
|
+
let chevronFrame = CGRect(x: chevronHorizontalInset, y: chevronVerticalInset, width: chevronWidth - chevronHorizontalInset * 2, height: size.height - chevronVerticalInset * 2)
|
|
102
|
+
|
|
103
|
+
leadingChevronImageView.frame = chevronFrame
|
|
104
|
+
trailingChevronView.frame = chevronFrame
|
|
105
|
+
|
|
106
|
+
leadingGrabber.frame = leadingView.frame
|
|
107
|
+
trailingGrabber.frame = trailingView.frame
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
override init(frame: CGRect) {
|
|
111
|
+
super.init(frame: frame)
|
|
112
|
+
setup()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
required init?(coder: NSCoder) {
|
|
116
|
+
super.init(coder: coder)
|
|
117
|
+
setup()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
//
|
|
2
|
+
// VideoTrimmerViewController.swift
|
|
3
|
+
// Shakebug
|
|
4
|
+
//
|
|
5
|
+
// Created by ideveloper2 on 26/04/24.
|
|
6
|
+
// Copyright © 2024 Facebook. All rights reserved.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import UIKit
|
|
10
|
+
import AVKit
|
|
11
|
+
|
|
12
|
+
extension CMTime {
|
|
13
|
+
var displayString: String {
|
|
14
|
+
let offset = TimeInterval(seconds)
|
|
15
|
+
let numberOfNanosecondsFloat = (offset - TimeInterval(Int(offset))) * 1000.0
|
|
16
|
+
let nanoseconds = Int(numberOfNanosecondsFloat)
|
|
17
|
+
let formatter = DateComponentsFormatter()
|
|
18
|
+
formatter.unitsStyle = .positional
|
|
19
|
+
formatter.zeroFormattingBehavior = .pad
|
|
20
|
+
formatter.allowedUnits = [.minute, .second]
|
|
21
|
+
return String(format: "%@.%03d", formatter.string(from: offset) ?? "00:00", nanoseconds)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
extension AVAsset {
|
|
26
|
+
var fullRange: CMTimeRange {
|
|
27
|
+
return CMTimeRange(start: .zero, duration: duration)
|
|
28
|
+
}
|
|
29
|
+
func trimmedComposition(_ range: CMTimeRange) -> AVAsset {
|
|
30
|
+
guard CMTimeRangeEqual(fullRange, range) == false else {return self}
|
|
31
|
+
|
|
32
|
+
let composition = AVMutableComposition()
|
|
33
|
+
try? composition.insertTimeRange(range, of: self, at: .zero)
|
|
34
|
+
|
|
35
|
+
if let videoTrack = tracks(withMediaType: .video).first {
|
|
36
|
+
composition.tracks.forEach {$0.preferredTransform = videoTrack.preferredTransform}
|
|
37
|
+
}
|
|
38
|
+
return composition
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@available(iOS 13.0, *)
|
|
43
|
+
class VideoTrimmerViewController: UIViewController {
|
|
44
|
+
var asset: AVAsset!
|
|
45
|
+
var maximumDuration: Int?
|
|
46
|
+
var minimumDuration: Int?
|
|
47
|
+
var cancelBtnText = "Cancel"
|
|
48
|
+
var saveButtonText = "Save"
|
|
49
|
+
var cancelBtnClicked: (() -> Void)?
|
|
50
|
+
var saveBtnClicked: ((CMTimeRange) -> Void)?
|
|
51
|
+
|
|
52
|
+
let playerController = AVPlayerViewController()
|
|
53
|
+
var trimmer: VideoTrimmer!
|
|
54
|
+
var timingStackView: UIStackView!
|
|
55
|
+
var leadingTrimLabel: UILabel!
|
|
56
|
+
var currentTimeLabel: UILabel!
|
|
57
|
+
var trailingTrimLabel: UILabel!
|
|
58
|
+
|
|
59
|
+
private var btnStackView: UIStackView!
|
|
60
|
+
private var cancelBtn: UIButton!
|
|
61
|
+
private var playBtn: UIButton!
|
|
62
|
+
private var saveBtn: UIButton!
|
|
63
|
+
private let playIcon = UIImage(systemName: "play.fill")
|
|
64
|
+
private let pauseIcon = UIImage(systemName: "pause.fill")
|
|
65
|
+
|
|
66
|
+
private var wasPlaying = false
|
|
67
|
+
private var player: AVPlayer! {playerController.player}
|
|
68
|
+
private var timeObserverToken: Any?
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// MARK: - Input
|
|
72
|
+
@objc private func didBeginTrimming(_ sender: VideoTrimmer) {
|
|
73
|
+
updateLabels()
|
|
74
|
+
|
|
75
|
+
wasPlaying = (player.timeControlStatus != .paused)
|
|
76
|
+
player.pause()
|
|
77
|
+
|
|
78
|
+
updatePlayerAsset()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@objc private func didEndTrimming(_ sender: VideoTrimmer) {
|
|
82
|
+
updateLabels()
|
|
83
|
+
|
|
84
|
+
updatePlayerAsset()
|
|
85
|
+
|
|
86
|
+
if wasPlaying == true {
|
|
87
|
+
player.play()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@objc private func selectedRangeDidChanged(_ sender: VideoTrimmer) {
|
|
92
|
+
updateLabels()
|
|
93
|
+
if trimmer.trimmingState == .leading {
|
|
94
|
+
player.seek(to: trimmer.selectedRange.start, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
95
|
+
} else if trimmer.trimmingState == .trailing {
|
|
96
|
+
player.seek(to: trimmer.selectedRange.end, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@objc private func didBeginScrubbing(_ sender: VideoTrimmer) {
|
|
101
|
+
updateLabels()
|
|
102
|
+
|
|
103
|
+
wasPlaying = (player.timeControlStatus != .paused)
|
|
104
|
+
player.pause()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@objc private func didEndScrubbing(_ sender: VideoTrimmer) {
|
|
108
|
+
updateLabels()
|
|
109
|
+
|
|
110
|
+
if wasPlaying == true {
|
|
111
|
+
player.play()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@objc private func progressDidChanged(_ sender: VideoTrimmer) {
|
|
116
|
+
updateLabels()
|
|
117
|
+
|
|
118
|
+
player.seek(to: trimmer.progress, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// MARK: - Private
|
|
122
|
+
private func updateLabels() {
|
|
123
|
+
leadingTrimLabel.text = trimmer.selectedRange.start.displayString
|
|
124
|
+
currentTimeLabel.text = trimmer.progress.displayString
|
|
125
|
+
trailingTrimLabel.text = trimmer.selectedRange.end.displayString
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private func updatePlayerAsset() {
|
|
129
|
+
if player.currentItem == nil {
|
|
130
|
+
let item = AVPlayerItem(asset: asset)
|
|
131
|
+
player.replaceCurrentItem(with: item)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if trimmer.trimmingState == .none {
|
|
135
|
+
player.currentItem?.forwardPlaybackEndTime = trimmer.selectedRange.end
|
|
136
|
+
|
|
137
|
+
let currentTime = player.currentTime().seconds
|
|
138
|
+
let endTime = trimmer.selectedRange.end.seconds
|
|
139
|
+
let startTime = trimmer.selectedRange.start.seconds
|
|
140
|
+
|
|
141
|
+
if currentTime < startTime || currentTime >= endTime - 0.05 {
|
|
142
|
+
player.seek(to: trimmer.selectedRange.start, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
player.currentItem?.forwardPlaybackEndTime = .invalid
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// MARK: - UIViewController
|
|
150
|
+
override func viewDidLoad() {
|
|
151
|
+
super.viewDidLoad()
|
|
152
|
+
|
|
153
|
+
view.backgroundColor = .black
|
|
154
|
+
|
|
155
|
+
// bottom action buttons
|
|
156
|
+
cancelBtn = UIButton(type: .system)
|
|
157
|
+
cancelBtn.setTitle(cancelBtnText, for: .normal)
|
|
158
|
+
cancelBtn.titleLabel?.font = .systemFont(ofSize: 18)
|
|
159
|
+
cancelBtn.setTitleColor(.white, for: .normal)
|
|
160
|
+
cancelBtn.addTarget(self, action: #selector(onCancelBtnClicked), for: .touchUpInside)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
playBtn = UIButton(type: .system)
|
|
164
|
+
playBtn.setImage(playIcon, for: .normal)
|
|
165
|
+
playBtn.tintColor = .systemBlue
|
|
166
|
+
playBtn.addTarget(self, action: #selector(togglePlay(sender:)), for: .touchUpInside)
|
|
167
|
+
|
|
168
|
+
saveBtn = UIButton(type: .system)
|
|
169
|
+
saveBtn.setTitle(saveButtonText, for: .normal)
|
|
170
|
+
saveBtn.titleLabel?.font = .systemFont(ofSize: 18)
|
|
171
|
+
saveBtn.setTitleColor(.systemBlue, for: .normal)
|
|
172
|
+
saveBtn.addTarget(self, action: #selector(onSaveBtnClicked), for: .touchUpInside)
|
|
173
|
+
|
|
174
|
+
btnStackView = UIStackView(arrangedSubviews: [cancelBtn, playBtn, saveBtn])
|
|
175
|
+
btnStackView.axis = .horizontal
|
|
176
|
+
btnStackView.alignment = .fill
|
|
177
|
+
btnStackView.distribution = .fillEqually
|
|
178
|
+
btnStackView.spacing = UIStackView.spacingUseSystem
|
|
179
|
+
view.addSubview(btnStackView)
|
|
180
|
+
btnStackView.translatesAutoresizingMaskIntoConstraints = false
|
|
181
|
+
NSLayoutConstraint.activate([
|
|
182
|
+
btnStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
|
|
183
|
+
btnStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
|
|
184
|
+
btnStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
// time labels
|
|
188
|
+
leadingTrimLabel = UILabel()
|
|
189
|
+
leadingTrimLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
|
|
190
|
+
leadingTrimLabel.textAlignment = .left
|
|
191
|
+
leadingTrimLabel.textColor = .white
|
|
192
|
+
|
|
193
|
+
currentTimeLabel = UILabel()
|
|
194
|
+
currentTimeLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
|
|
195
|
+
currentTimeLabel.textAlignment = .center
|
|
196
|
+
currentTimeLabel.textColor = .white
|
|
197
|
+
|
|
198
|
+
trailingTrimLabel = UILabel()
|
|
199
|
+
trailingTrimLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
|
|
200
|
+
trailingTrimLabel.textAlignment = .right
|
|
201
|
+
trailingTrimLabel.textColor = .white
|
|
202
|
+
|
|
203
|
+
timingStackView = UIStackView(arrangedSubviews: [leadingTrimLabel, currentTimeLabel, trailingTrimLabel])
|
|
204
|
+
timingStackView.axis = .horizontal
|
|
205
|
+
timingStackView.alignment = .fill
|
|
206
|
+
timingStackView.distribution = .fillEqually
|
|
207
|
+
timingStackView.spacing = UIStackView.spacingUseSystem
|
|
208
|
+
view.addSubview(timingStackView)
|
|
209
|
+
timingStackView.translatesAutoresizingMaskIntoConstraints = false
|
|
210
|
+
NSLayoutConstraint.activate([
|
|
211
|
+
timingStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
|
|
212
|
+
timingStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
|
|
213
|
+
timingStackView.bottomAnchor.constraint(equalTo: btnStackView.topAnchor, constant: -8),
|
|
214
|
+
])
|
|
215
|
+
|
|
216
|
+
// THIS IS WHERE WE SETUP THE VIDEOTRIMMER:
|
|
217
|
+
trimmer = VideoTrimmer()
|
|
218
|
+
trimmer.asset = asset // this should happen before trimmer.selectedRange below otherwise its didSet will override
|
|
219
|
+
trimmer.minimumDuration = CMTime(seconds: 1, preferredTimescale: 600)
|
|
220
|
+
|
|
221
|
+
if maximumDuration != nil {
|
|
222
|
+
trimmer.maximumDuration = CMTime(seconds: max(1, Double(maximumDuration!)), preferredTimescale: 600) // minimum 1 second
|
|
223
|
+
|
|
224
|
+
// guard check to make sure max duration can only <= asset.duration
|
|
225
|
+
if CMTimeCompare(trimmer.maximumDuration, asset.duration) == 1 {
|
|
226
|
+
trimmer.maximumDuration = asset.duration
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
trimmer.selectedRange = CMTimeRange(start: .zero, end: trimmer.maximumDuration)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if minimumDuration != nil {
|
|
233
|
+
trimmer.minimumDuration = CMTime(seconds: max(1, Double(minimumDuration!)), preferredTimescale: 600) // minimum 1 second
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
trimmer.addTarget(self, action: #selector(didBeginTrimming(_:)), for: VideoTrimmer.didBeginTrimming)
|
|
237
|
+
trimmer.addTarget(self, action: #selector(didEndTrimming(_:)), for: VideoTrimmer.didEndTrimming)
|
|
238
|
+
trimmer.addTarget(self, action: #selector(selectedRangeDidChanged(_:)), for: VideoTrimmer.selectedRangeChanged)
|
|
239
|
+
trimmer.addTarget(self, action: #selector(didBeginScrubbing(_:)), for: VideoTrimmer.didBeginScrubbing)
|
|
240
|
+
trimmer.addTarget(self, action: #selector(didEndScrubbing(_:)), for: VideoTrimmer.didEndScrubbing)
|
|
241
|
+
trimmer.addTarget(self, action: #selector(progressDidChanged(_:)), for: VideoTrimmer.progressChanged)
|
|
242
|
+
view.addSubview(trimmer)
|
|
243
|
+
trimmer.translatesAutoresizingMaskIntoConstraints = false
|
|
244
|
+
NSLayoutConstraint.activate([
|
|
245
|
+
trimmer.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
|
246
|
+
trimmer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
|
247
|
+
trimmer.bottomAnchor.constraint(equalTo: timingStackView.topAnchor, constant: -16),
|
|
248
|
+
trimmer.heightAnchor.constraint(equalToConstant: 50),
|
|
249
|
+
])
|
|
250
|
+
|
|
251
|
+
playerController.showsPlaybackControls = false // hide control buttons
|
|
252
|
+
if #available(iOS 16.0, *) {
|
|
253
|
+
playerController.allowsVideoFrameAnalysis = false // hide live text
|
|
254
|
+
}
|
|
255
|
+
playerController.player = AVPlayer()
|
|
256
|
+
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: []) // this is to play audio even when device is in silent mode
|
|
257
|
+
addChild(playerController)
|
|
258
|
+
view.addSubview(playerController.view)
|
|
259
|
+
playerController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
260
|
+
NSLayoutConstraint.activate([
|
|
261
|
+
playerController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
262
|
+
playerController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
263
|
+
playerController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
264
|
+
playerController.view.bottomAnchor.constraint(equalTo: trimmer.topAnchor, constant: -16)
|
|
265
|
+
])
|
|
266
|
+
|
|
267
|
+
updatePlayerAsset()
|
|
268
|
+
|
|
269
|
+
timeObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 30), queue: .main) { [weak self] time in
|
|
270
|
+
guard let self = self else {return}
|
|
271
|
+
|
|
272
|
+
self.currentTimeLabel.text = time.displayString
|
|
273
|
+
self.trimmer.progress = time
|
|
274
|
+
|
|
275
|
+
if self.player.timeControlStatus == .playing {
|
|
276
|
+
self.playBtn.setImage(self.pauseIcon, for: .normal)
|
|
277
|
+
} else {
|
|
278
|
+
self.playBtn.setImage(self.playIcon, for: .normal)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
updateLabels()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
override func viewWillDisappear(_ animated: Bool) {
|
|
286
|
+
super.viewWillDisappear(animated)
|
|
287
|
+
|
|
288
|
+
player.pause()
|
|
289
|
+
if timeObserverToken != nil {
|
|
290
|
+
player.removeTimeObserver(timeObserverToken as Any)
|
|
291
|
+
timeObserverToken = nil
|
|
292
|
+
}
|
|
293
|
+
playerController.player = nil
|
|
294
|
+
playerController.dismiss(animated: false, completion: nil)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@objc private func togglePlay(sender: UIButton) {
|
|
298
|
+
if player.timeControlStatus == .playing {
|
|
299
|
+
player.pause()
|
|
300
|
+
} else {
|
|
301
|
+
let currentTime = player.currentTime().seconds
|
|
302
|
+
let endTime = trimmer.selectedRange.end.seconds
|
|
303
|
+
let startTime = trimmer.selectedRange.start.seconds
|
|
304
|
+
|
|
305
|
+
if currentTime >= endTime - 0.05 || currentTime < startTime {
|
|
306
|
+
player.seek(to: trimmer.selectedRange.start, toleranceBefore: .zero, toleranceAfter: .zero) { _ in
|
|
307
|
+
self.player.play()
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
player.play()
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@objc func onSaveBtnClicked() {
|
|
316
|
+
saveBtnClicked?(trimmer.selectedRange)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@objc func onCancelBtnClicked() {
|
|
320
|
+
cancelBtnClicked?()
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -2,24 +2,40 @@
|
|
|
2
2
|
// VideoPlayerModule.m
|
|
3
3
|
// Shakebug
|
|
4
4
|
//
|
|
5
|
-
// Created by ideveloper2 on 23/04/24.
|
|
6
|
-
// Copyright © 2024 Facebook. All rights reserved.
|
|
7
|
-
//
|
|
8
5
|
|
|
9
6
|
#import "VideoPlayerModule.h"
|
|
10
7
|
#import <React/RCTLog.h>
|
|
8
|
+
#import <React/RCTUtils.h>
|
|
11
9
|
|
|
12
10
|
@implementation ShakebugSDKVideoPlayerModule
|
|
13
11
|
|
|
14
|
-
RCT_EXPORT_MODULE();
|
|
12
|
+
RCT_EXPORT_MODULE(ShakebugSDKVideoPlayerModule);
|
|
13
|
+
|
|
14
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
15
|
+
return YES;
|
|
16
|
+
}
|
|
15
17
|
|
|
16
18
|
RCT_EXPORT_METHOD(playVideo:(NSString *)videoURLString) {
|
|
17
|
-
|
|
19
|
+
if (!videoURLString || videoURLString.length == 0) {
|
|
20
|
+
RCTLogError(@"Invalid video URL");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
NSURL *videoURL;
|
|
25
|
+
if ([videoURLString hasPrefix:@"http://"] || [videoURLString hasPrefix:@"https://"]) {
|
|
26
|
+
videoURL = [NSURL URLWithString:videoURLString];
|
|
27
|
+
} else {
|
|
28
|
+
NSString *cleanPath = [videoURLString stringByReplacingOccurrencesOfString:@"file://" withString:@""];
|
|
29
|
+
videoURL = [NSURL fileURLWithPath:cleanPath];
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
if (videoURL) {
|
|
19
33
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
20
34
|
ShakebugSDKVideoPlayerViewController *videoPlayerVC = [[ShakebugSDKVideoPlayerViewController alloc] initWithVideoURL:videoURL];
|
|
21
|
-
UIViewController *rootViewController =
|
|
22
|
-
|
|
35
|
+
UIViewController *rootViewController = RCTPresentedViewController();
|
|
36
|
+
if (rootViewController) {
|
|
37
|
+
[rootViewController presentViewController:videoPlayerVC animated:YES completion:nil];
|
|
38
|
+
}
|
|
23
39
|
});
|
|
24
40
|
} else {
|
|
25
41
|
RCTLogError(@"Invalid video URL");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shakebug-react-native",
|
|
3
3
|
"title": "shakebug-react-native",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.10",
|
|
5
5
|
"description": "Shakebug allows users to report the bug from your mobile phone for your application. Here users just need to shake their mobile and all the data regarding bugs & crashes can be seen by developers through their log in panel easily",
|
|
6
6
|
"main": "src/index.tsx",
|
|
7
7
|
"types": "src/index.tsx",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, ActivityIndicator, StyleSheet, Text } from 'react-native';
|
|
3
|
+
|
|
4
|
+
const LoadingModal = ({ visible, color = '#1dd3e3', text = '' }) => {
|
|
5
|
+
if (!visible) return null;
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<View style={styles.overlay} pointerEvents="auto">
|
|
9
|
+
<View style={styles.backdrop} />
|
|
10
|
+
<View style={styles.indicatorContainer}>
|
|
11
|
+
<ActivityIndicator size="large" color={color} />
|
|
12
|
+
{text ? <Text style={styles.text}>{text}</Text> : null}
|
|
13
|
+
</View>
|
|
14
|
+
</View>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const styles = StyleSheet.create({
|
|
19
|
+
overlay: {
|
|
20
|
+
position: 'absolute',
|
|
21
|
+
top: 0,
|
|
22
|
+
bottom: 0,
|
|
23
|
+
left: 0,
|
|
24
|
+
right: 0,
|
|
25
|
+
zIndex: 99999,
|
|
26
|
+
elevation: 999,
|
|
27
|
+
justifyContent: 'center',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
},
|
|
30
|
+
backdrop: {
|
|
31
|
+
position: 'absolute',
|
|
32
|
+
top: 0,
|
|
33
|
+
bottom: 0,
|
|
34
|
+
left: 0,
|
|
35
|
+
right: 0,
|
|
36
|
+
backgroundColor: 'rgba(0, 0, 0, 0.65)',
|
|
37
|
+
},
|
|
38
|
+
indicatorContainer: {
|
|
39
|
+
padding: 24,
|
|
40
|
+
backgroundColor: '#ffffff',
|
|
41
|
+
borderRadius: 16,
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
shadowColor: '#000',
|
|
45
|
+
shadowOffset: { width: 0, height: 4 },
|
|
46
|
+
shadowOpacity: 0.35,
|
|
47
|
+
shadowRadius: 8,
|
|
48
|
+
elevation: 10,
|
|
49
|
+
zIndex: 100000,
|
|
50
|
+
},
|
|
51
|
+
text: {
|
|
52
|
+
marginTop: 12,
|
|
53
|
+
fontSize: 14,
|
|
54
|
+
color: '#333333',
|
|
55
|
+
fontWeight: '500',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export default LoadingModal;
|
|
@@ -13,6 +13,7 @@ console.ignoredYellowBox = ["Warning: Each", "Warning: Failed", "Possible", "WAR
|
|
|
13
13
|
import { logger, consoleTransport } from "react-native-logs";
|
|
14
14
|
import { ActivityIndicator } from 'react-native';
|
|
15
15
|
import { useGlobalState } from './GlobalStateContext';
|
|
16
|
+
import LoadingModal from './LoadingModal';
|
|
16
17
|
|
|
17
18
|
LogBox.ignoreAllLogs()
|
|
18
19
|
|
|
@@ -523,16 +524,7 @@ const ShakeFunction = ({ children, themeColor, showTutorialScreen, SDKScreenTitl
|
|
|
523
524
|
|
|
524
525
|
</ShakebugCapture>
|
|
525
526
|
|
|
526
|
-
{isLoading
|
|
527
|
-
<View
|
|
528
|
-
style={{
|
|
529
|
-
...StyleSheet.absoluteFillObject,
|
|
530
|
-
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
531
|
-
}}
|
|
532
|
-
/>
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
{isLoading && <ActivityIndicator size={'large'} color={themeColor} collapsable={true} style={{ position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, elevation: 2 }} />}
|
|
527
|
+
<LoadingModal visible={isLoading} color={themeColor} />
|
|
536
528
|
{isShow ? (
|
|
537
529
|
<Animated.View
|
|
538
530
|
{...panResponder.panHandlers}
|