react-native-video-trim 7.0.0 → 7.0.1
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/android/src/main/java/com/videotrim/widgets/CropOverlayView.kt +17 -5
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.kt +44 -26
- package/android/src/main/res/layout/video_trimmer_view.xml +7 -5
- package/ios/CropOverlayView.swift +38 -36
- package/ios/VideoTrimmerViewController.swift +5 -4
- package/package.json +1 -1
|
@@ -50,12 +50,13 @@ class CropOverlayView @JvmOverloads constructor(
|
|
|
50
50
|
private val borderWidth = dpToPx(1f)
|
|
51
51
|
private val cornerLength = dpToPx(20f)
|
|
52
52
|
private val cornerWidth = dpToPx(4f)
|
|
53
|
+
private val edgeHandleLength = dpToPx(20f)
|
|
53
54
|
private val gridLineWidth = 1f / resources.displayMetrics.density
|
|
54
55
|
private val edgeHitZone = dpToPx(30f)
|
|
55
56
|
|
|
56
57
|
private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
57
58
|
style = Paint.Style.STROKE
|
|
58
|
-
color = Color.
|
|
59
|
+
color = Color.WHITE
|
|
59
60
|
strokeWidth = borderWidth
|
|
60
61
|
}
|
|
61
62
|
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
@@ -68,6 +69,7 @@ class CropOverlayView @JvmOverloads constructor(
|
|
|
68
69
|
color = Color.WHITE
|
|
69
70
|
strokeWidth = cornerWidth
|
|
70
71
|
strokeCap = Paint.Cap.ROUND
|
|
72
|
+
strokeJoin = Paint.Join.ROUND
|
|
71
73
|
}
|
|
72
74
|
private val dimmingPaint = Paint().apply {
|
|
73
75
|
color = Color.argb(140, 0, 0, 0)
|
|
@@ -140,14 +142,24 @@ class CropOverlayView @JvmOverloads constructor(
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
val cl = cornerLength
|
|
145
|
+
val hw = cornerWidth / 2f
|
|
143
146
|
val path = Path()
|
|
144
147
|
fun addCorner(sx: Float, sy: Float, cx: Float, cy: Float, ex: Float, ey: Float) {
|
|
145
148
|
path.moveTo(sx, sy); path.lineTo(cx, cy); path.lineTo(ex, ey)
|
|
146
149
|
}
|
|
147
|
-
addCorner(cr.left, cr.top + cl, cr.left, cr.top, cr.left + cl, cr.top)
|
|
148
|
-
addCorner(cr.right - cl, cr.top, cr.right, cr.top, cr.right, cr.top + cl)
|
|
149
|
-
addCorner(cr.left, cr.bottom - cl, cr.left, cr.bottom, cr.left + cl, cr.bottom)
|
|
150
|
-
addCorner(cr.right - cl, cr.bottom, cr.right, cr.bottom, cr.right, cr.bottom - cl)
|
|
150
|
+
addCorner(cr.left - hw, cr.top + cl, cr.left - hw, cr.top - hw, cr.left + cl, cr.top - hw)
|
|
151
|
+
addCorner(cr.right - cl, cr.top - hw, cr.right + hw, cr.top - hw, cr.right + hw, cr.top + cl)
|
|
152
|
+
addCorner(cr.left - hw, cr.bottom - cl, cr.left - hw, cr.bottom + hw, cr.left + cl, cr.bottom + hw)
|
|
153
|
+
addCorner(cr.right - cl, cr.bottom + hw, cr.right + hw, cr.bottom + hw, cr.right + hw, cr.bottom - cl)
|
|
154
|
+
|
|
155
|
+
val ehl = edgeHandleLength / 2f
|
|
156
|
+
val cx = cr.centerX()
|
|
157
|
+
val cy = cr.centerY()
|
|
158
|
+
path.moveTo(cx - ehl, cr.top - hw); path.lineTo(cx + ehl, cr.top - hw)
|
|
159
|
+
path.moveTo(cx - ehl, cr.bottom + hw); path.lineTo(cx + ehl, cr.bottom + hw)
|
|
160
|
+
path.moveTo(cr.left - hw, cy - ehl); path.lineTo(cr.left - hw, cy + ehl)
|
|
161
|
+
path.moveTo(cr.right + hw, cy - ehl); path.lineTo(cr.right + hw, cy + ehl)
|
|
162
|
+
|
|
151
163
|
canvas.drawPath(path, cornerPaint)
|
|
152
164
|
}
|
|
153
165
|
|
|
@@ -320,20 +320,25 @@ class VideoTrimmerView(
|
|
|
320
320
|
if (vw <= 0 || vh <= 0) return
|
|
321
321
|
|
|
322
322
|
videoContainer.post {
|
|
323
|
-
val
|
|
324
|
-
val
|
|
325
|
-
if (
|
|
323
|
+
val cw = containerContentWidth().toInt()
|
|
324
|
+
val ch = containerContentHeight().toInt()
|
|
325
|
+
if (cw <= 0 || ch <= 0) return@post
|
|
326
|
+
|
|
327
|
+
val margin = bracketOverflow()
|
|
328
|
+
val availW = cw - 2 * margin
|
|
329
|
+
val availH = ch - 2 * margin
|
|
330
|
+
if (availW <= 0 || availH <= 0) return@post
|
|
326
331
|
|
|
327
332
|
val videoAR = vw.toFloat() / vh
|
|
328
|
-
val containerAR =
|
|
333
|
+
val containerAR = availW.toFloat() / availH
|
|
329
334
|
val newW: Int
|
|
330
335
|
val newH: Int
|
|
331
336
|
if (videoAR > containerAR) {
|
|
332
|
-
newW =
|
|
333
|
-
newH = (
|
|
337
|
+
newW = availW
|
|
338
|
+
newH = (availW / videoAR).toInt()
|
|
334
339
|
} else {
|
|
335
|
-
newH =
|
|
336
|
-
newW = (
|
|
340
|
+
newH = availH
|
|
341
|
+
newW = (availH * videoAR).toInt()
|
|
337
342
|
}
|
|
338
343
|
mVideoView.layoutParams = FrameLayout.LayoutParams(newW, newH, Gravity.CENTER)
|
|
339
344
|
}
|
|
@@ -519,6 +524,10 @@ class VideoTrimmerView(
|
|
|
519
524
|
cropBtn.setOnClickListener { onCropTapped() }
|
|
520
525
|
undoBtn.setOnClickListener { onUndoTapped() }
|
|
521
526
|
redoBtn.setOnClickListener { onRedoTapped() }
|
|
527
|
+
|
|
528
|
+
cropBtn.setColorFilter(Color.argb(128, 255, 255, 255), android.graphics.PorterDuff.Mode.SRC_IN)
|
|
529
|
+
undoBtn.setColorFilter(Color.argb(128, 255, 255, 255), android.graphics.PorterDuff.Mode.SRC_IN)
|
|
530
|
+
redoBtn.setColorFilter(Color.argb(128, 255, 255, 255), android.graphics.PorterDuff.Mode.SRC_IN)
|
|
522
531
|
}
|
|
523
532
|
|
|
524
533
|
fun onSaveClicked() {
|
|
@@ -1304,13 +1313,22 @@ class VideoTrimmerView(
|
|
|
1304
1313
|
|
|
1305
1314
|
// region Transform
|
|
1306
1315
|
|
|
1316
|
+
private fun containerContentWidth(): Float =
|
|
1317
|
+
(videoContainer.width - videoContainer.paddingLeft - videoContainer.paddingRight).toFloat()
|
|
1318
|
+
|
|
1319
|
+
private fun containerContentHeight(): Float =
|
|
1320
|
+
(videoContainer.height - videoContainer.paddingTop - videoContainer.paddingBottom).toFloat()
|
|
1321
|
+
|
|
1322
|
+
private fun bracketOverflow(): Int =
|
|
1323
|
+
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4f, resources.displayMetrics).toInt()
|
|
1324
|
+
|
|
1307
1325
|
private fun onFlipTapped() {
|
|
1308
1326
|
pushUndo()
|
|
1309
1327
|
isFlipped = !isFlipped
|
|
1310
1328
|
val newCumDeg = -cumulativeRotationDeg
|
|
1311
1329
|
val fitScale = if (rotationCount % 2 != 0) {
|
|
1312
|
-
val cw =
|
|
1313
|
-
val ch =
|
|
1330
|
+
val cw = containerContentWidth()
|
|
1331
|
+
val ch = containerContentHeight()
|
|
1314
1332
|
if (cw > 0 && ch > 0) minOf(cw / ch, ch / cw) else 1f
|
|
1315
1333
|
} else {
|
|
1316
1334
|
1f
|
|
@@ -1379,12 +1397,12 @@ class VideoTrimmerView(
|
|
|
1379
1397
|
}
|
|
1380
1398
|
|
|
1381
1399
|
private fun updateVideoTransform(resetCrop: Boolean = false) {
|
|
1382
|
-
val
|
|
1383
|
-
val
|
|
1384
|
-
if (
|
|
1400
|
+
val cw = containerContentWidth()
|
|
1401
|
+
val ch = containerContentHeight()
|
|
1402
|
+
if (cw <= 0 || ch <= 0) return
|
|
1385
1403
|
|
|
1386
1404
|
val fitScale = if (rotationCount % 2 != 0) {
|
|
1387
|
-
minOf(
|
|
1405
|
+
minOf(cw / ch, ch / cw)
|
|
1388
1406
|
} else {
|
|
1389
1407
|
1f
|
|
1390
1408
|
}
|
|
@@ -1487,22 +1505,22 @@ class VideoTrimmerView(
|
|
|
1487
1505
|
}
|
|
1488
1506
|
|
|
1489
1507
|
private fun getVideoDisplayRectInContainer(): RectF {
|
|
1490
|
-
val
|
|
1491
|
-
val
|
|
1492
|
-
if (
|
|
1508
|
+
val cw = containerContentWidth()
|
|
1509
|
+
val ch = containerContentHeight()
|
|
1510
|
+
if (cw <= 0 || ch <= 0) return RectF()
|
|
1493
1511
|
|
|
1494
1512
|
val tvW = mVideoView.width.toFloat()
|
|
1495
1513
|
val tvH = mVideoView.height.toFloat()
|
|
1496
1514
|
if (tvW <= 0 || tvH <= 0) return RectF()
|
|
1497
1515
|
|
|
1498
|
-
val tvX = (
|
|
1499
|
-
val tvY = (
|
|
1516
|
+
val tvX = (cw - tvW) / 2f
|
|
1517
|
+
val tvY = (ch - tvH) / 2f
|
|
1500
1518
|
val videoRect = RectF(tvX, tvY, tvX + tvW, tvY + tvH)
|
|
1501
1519
|
|
|
1502
|
-
val pivotX =
|
|
1503
|
-
val pivotY =
|
|
1520
|
+
val pivotX = cw / 2f
|
|
1521
|
+
val pivotY = ch / 2f
|
|
1504
1522
|
val fitScale = if (rotationCount % 2 != 0) {
|
|
1505
|
-
minOf(
|
|
1523
|
+
minOf(cw / ch, ch / cw)
|
|
1506
1524
|
} else {
|
|
1507
1525
|
1f
|
|
1508
1526
|
}
|
|
@@ -1603,10 +1621,10 @@ class VideoTrimmerView(
|
|
|
1603
1621
|
isFlipped = snap.isFlipped
|
|
1604
1622
|
cumulativeRotationDeg = snap.cumulativeRotationDeg
|
|
1605
1623
|
|
|
1606
|
-
val
|
|
1607
|
-
val
|
|
1608
|
-
val fitScale = if (rotationCount % 2 != 0 &&
|
|
1609
|
-
minOf(
|
|
1624
|
+
val cw = containerContentWidth()
|
|
1625
|
+
val ch = containerContentHeight()
|
|
1626
|
+
val fitScale = if (rotationCount % 2 != 0 && cw > 0 && ch > 0) {
|
|
1627
|
+
minOf(cw / ch, ch / cw)
|
|
1610
1628
|
} else {
|
|
1611
1629
|
1f
|
|
1612
1630
|
}
|
|
@@ -94,8 +94,8 @@
|
|
|
94
94
|
|
|
95
95
|
<ImageView
|
|
96
96
|
android:id="@+id/undoBtn"
|
|
97
|
-
android:layout_width="
|
|
98
|
-
android:layout_height="
|
|
97
|
+
android:layout_width="18dp"
|
|
98
|
+
android:layout_height="18dp"
|
|
99
99
|
android:src="@drawable/arrow_uturn_backward"
|
|
100
100
|
android:tint="#80FFFFFF"
|
|
101
101
|
android:scaleType="fitCenter"
|
|
@@ -104,8 +104,8 @@
|
|
|
104
104
|
|
|
105
105
|
<ImageView
|
|
106
106
|
android:id="@+id/redoBtn"
|
|
107
|
-
android:layout_width="
|
|
108
|
-
android:layout_height="
|
|
107
|
+
android:layout_width="18dp"
|
|
108
|
+
android:layout_height="18dp"
|
|
109
109
|
android:src="@drawable/arrow_uturn_forward"
|
|
110
110
|
android:tint="#80FFFFFF"
|
|
111
111
|
android:scaleType="fitCenter"
|
|
@@ -120,7 +120,9 @@
|
|
|
120
120
|
android:layout_above="@+id/layout"
|
|
121
121
|
android:layout_below="@+id/transformRow"
|
|
122
122
|
android:background="@android:color/black"
|
|
123
|
-
android:clipChildren="true"
|
|
123
|
+
android:clipChildren="true"
|
|
124
|
+
android:clipToPadding="false"
|
|
125
|
+
android:paddingVertical="4dp">
|
|
124
126
|
|
|
125
127
|
<TextureView
|
|
126
128
|
android:id="@+id/video_loader"
|
|
@@ -6,7 +6,6 @@ class CropOverlayView: UIView {
|
|
|
6
6
|
var cropRect: CGRect = .zero {
|
|
7
7
|
didSet {
|
|
8
8
|
setNeedsDisplay()
|
|
9
|
-
updateDimmingMask()
|
|
10
9
|
}
|
|
11
10
|
}
|
|
12
11
|
|
|
@@ -31,6 +30,7 @@ class CropOverlayView: UIView {
|
|
|
31
30
|
private let borderWidth: CGFloat = 1.0
|
|
32
31
|
private let cornerLength: CGFloat = 20
|
|
33
32
|
private let cornerWidth: CGFloat = 4.0
|
|
33
|
+
private let edgeHandleLength: CGFloat = 20
|
|
34
34
|
private let gridLineWidth: CGFloat = CGFloat(1.0 / UIScreen.main.scale)
|
|
35
35
|
private let edgeHitZone: CGFloat = 30
|
|
36
36
|
|
|
@@ -38,8 +38,6 @@ class CropOverlayView: UIView {
|
|
|
38
38
|
private var dragStart: CGPoint = .zero
|
|
39
39
|
private var dragStartRect: CGRect = .zero
|
|
40
40
|
|
|
41
|
-
private let dimmingLayer = CAShapeLayer()
|
|
42
|
-
|
|
43
41
|
private enum DragEdge {
|
|
44
42
|
case top, bottom, left, right
|
|
45
43
|
case topLeft, topRight, bottomLeft, bottomRight
|
|
@@ -59,13 +57,9 @@ class CropOverlayView: UIView {
|
|
|
59
57
|
private func commonInit() {
|
|
60
58
|
backgroundColor = .clear
|
|
61
59
|
isUserInteractionEnabled = true
|
|
62
|
-
clipsToBounds =
|
|
60
|
+
clipsToBounds = false
|
|
63
61
|
isOpaque = false
|
|
64
62
|
|
|
65
|
-
dimmingLayer.fillRule = .evenOdd
|
|
66
|
-
dimmingLayer.fillColor = UIColor.black.withAlphaComponent(0.55).cgColor
|
|
67
|
-
layer.addSublayer(dimmingLayer)
|
|
68
|
-
|
|
69
63
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
|
70
64
|
addGestureRecognizer(pan)
|
|
71
65
|
|
|
@@ -73,32 +67,26 @@ class CropOverlayView: UIView {
|
|
|
73
67
|
addGestureRecognizer(pinch)
|
|
74
68
|
}
|
|
75
69
|
|
|
76
|
-
override func layoutSubviews() {
|
|
77
|
-
super.layoutSubviews()
|
|
78
|
-
updateDimmingMask()
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private func updateDimmingMask() {
|
|
82
|
-
let full = UIBezierPath(rect: bounds)
|
|
83
|
-
if !cropRect.isEmpty {
|
|
84
|
-
full.append(UIBezierPath(rect: cropRect))
|
|
85
|
-
}
|
|
86
|
-
dimmingLayer.path = full.cgPath
|
|
87
|
-
dimmingLayer.frame = bounds
|
|
88
|
-
}
|
|
89
|
-
|
|
90
70
|
// MARK: - Drawing
|
|
91
71
|
|
|
92
72
|
override func draw(_ rect: CGRect) {
|
|
93
73
|
guard !cropRect.isEmpty, let ctx = UIGraphicsGetCurrentContext() else { return }
|
|
94
74
|
let cr = cropRect
|
|
95
75
|
|
|
96
|
-
ctx.
|
|
76
|
+
ctx.saveGState()
|
|
77
|
+
let fullPath = UIBezierPath(rect: bounds)
|
|
78
|
+
fullPath.append(UIBezierPath(rect: cr))
|
|
79
|
+
fullPath.usesEvenOddFillRule = true
|
|
80
|
+
ctx.addPath(fullPath.cgPath)
|
|
81
|
+
ctx.setFillColor(UIColor.black.withAlphaComponent(0.55).cgColor)
|
|
82
|
+
ctx.fillPath(using: .evenOdd)
|
|
83
|
+
ctx.restoreGState()
|
|
84
|
+
|
|
85
|
+
ctx.setStrokeColor(UIColor.white.cgColor)
|
|
97
86
|
ctx.setLineWidth(borderWidth)
|
|
98
87
|
ctx.stroke(cr)
|
|
99
88
|
|
|
100
89
|
ctx.setLineWidth(gridLineWidth)
|
|
101
|
-
ctx.setStrokeColor(UIColor.white.cgColor)
|
|
102
90
|
for i in 1...2 {
|
|
103
91
|
let x = cr.minX + cr.width * CGFloat(i) / 3
|
|
104
92
|
ctx.move(to: CGPoint(x: x, y: cr.minY))
|
|
@@ -114,27 +102,41 @@ class CropOverlayView: UIView {
|
|
|
114
102
|
ctx.setStrokeColor(UIColor.white.cgColor)
|
|
115
103
|
ctx.setLineWidth(cornerWidth)
|
|
116
104
|
ctx.setLineCap(.round)
|
|
105
|
+
ctx.setLineJoin(.round)
|
|
117
106
|
|
|
118
107
|
let cl = cornerLength
|
|
108
|
+
let hw = cornerWidth / 2
|
|
119
109
|
let corners: [(CGPoint, CGPoint, CGPoint)] = [
|
|
120
|
-
(CGPoint(x: cr.minX, y: cr.minY + cl),
|
|
121
|
-
CGPoint(x: cr.minX, y: cr.minY),
|
|
122
|
-
CGPoint(x: cr.minX + cl, y: cr.minY)),
|
|
123
|
-
(CGPoint(x: cr.maxX - cl, y: cr.minY),
|
|
124
|
-
CGPoint(x: cr.maxX, y: cr.minY),
|
|
125
|
-
CGPoint(x: cr.maxX, y: cr.minY + cl)),
|
|
126
|
-
(CGPoint(x: cr.minX, y: cr.maxY - cl),
|
|
127
|
-
CGPoint(x: cr.minX, y: cr.maxY),
|
|
128
|
-
CGPoint(x: cr.minX + cl, y: cr.maxY)),
|
|
129
|
-
(CGPoint(x: cr.maxX - cl, y: cr.maxY),
|
|
130
|
-
CGPoint(x: cr.maxX, y: cr.maxY),
|
|
131
|
-
CGPoint(x: cr.maxX, y: cr.maxY - cl)),
|
|
110
|
+
(CGPoint(x: cr.minX - hw, y: cr.minY + cl),
|
|
111
|
+
CGPoint(x: cr.minX - hw, y: cr.minY - hw),
|
|
112
|
+
CGPoint(x: cr.minX + cl, y: cr.minY - hw)),
|
|
113
|
+
(CGPoint(x: cr.maxX - cl, y: cr.minY - hw),
|
|
114
|
+
CGPoint(x: cr.maxX + hw, y: cr.minY - hw),
|
|
115
|
+
CGPoint(x: cr.maxX + hw, y: cr.minY + cl)),
|
|
116
|
+
(CGPoint(x: cr.minX - hw, y: cr.maxY - cl),
|
|
117
|
+
CGPoint(x: cr.minX - hw, y: cr.maxY + hw),
|
|
118
|
+
CGPoint(x: cr.minX + cl, y: cr.maxY + hw)),
|
|
119
|
+
(CGPoint(x: cr.maxX - cl, y: cr.maxY + hw),
|
|
120
|
+
CGPoint(x: cr.maxX + hw, y: cr.maxY + hw),
|
|
121
|
+
CGPoint(x: cr.maxX + hw, y: cr.maxY - cl)),
|
|
132
122
|
]
|
|
133
123
|
for (start, corner, end) in corners {
|
|
134
124
|
ctx.move(to: start)
|
|
135
125
|
ctx.addLine(to: corner)
|
|
136
126
|
ctx.addLine(to: end)
|
|
137
127
|
}
|
|
128
|
+
|
|
129
|
+
let ehl = edgeHandleLength / 2
|
|
130
|
+
let cx = cr.midX, cy = cr.midY
|
|
131
|
+
ctx.move(to: CGPoint(x: cx - ehl, y: cr.minY - hw))
|
|
132
|
+
ctx.addLine(to: CGPoint(x: cx + ehl, y: cr.minY - hw))
|
|
133
|
+
ctx.move(to: CGPoint(x: cx - ehl, y: cr.maxY + hw))
|
|
134
|
+
ctx.addLine(to: CGPoint(x: cx + ehl, y: cr.maxY + hw))
|
|
135
|
+
ctx.move(to: CGPoint(x: cr.minX - hw, y: cy - ehl))
|
|
136
|
+
ctx.addLine(to: CGPoint(x: cr.minX - hw, y: cy + ehl))
|
|
137
|
+
ctx.move(to: CGPoint(x: cr.maxX + hw, y: cy - ehl))
|
|
138
|
+
ctx.addLine(to: CGPoint(x: cr.maxX + hw, y: cy + ehl))
|
|
139
|
+
|
|
138
140
|
ctx.strokePath()
|
|
139
141
|
}
|
|
140
142
|
|
|
@@ -469,11 +469,12 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
469
469
|
addChild(playerController)
|
|
470
470
|
playerContainerView.addSubview(playerController.view)
|
|
471
471
|
playerController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
472
|
+
let bracketInset: CGFloat = 4
|
|
472
473
|
NSLayoutConstraint.activate([
|
|
473
|
-
playerController.view.leadingAnchor.constraint(equalTo: playerContainerView.leadingAnchor),
|
|
474
|
-
playerController.view.trailingAnchor.constraint(equalTo: playerContainerView.trailingAnchor),
|
|
475
|
-
playerController.view.topAnchor.constraint(equalTo: playerContainerView.topAnchor),
|
|
476
|
-
playerController.view.bottomAnchor.constraint(equalTo: playerContainerView.bottomAnchor)
|
|
474
|
+
playerController.view.leadingAnchor.constraint(equalTo: playerContainerView.leadingAnchor, constant: bracketInset),
|
|
475
|
+
playerController.view.trailingAnchor.constraint(equalTo: playerContainerView.trailingAnchor, constant: -bracketInset),
|
|
476
|
+
playerController.view.topAnchor.constraint(equalTo: playerContainerView.topAnchor, constant: bracketInset),
|
|
477
|
+
playerController.view.bottomAnchor.constraint(equalTo: playerContainerView.bottomAnchor, constant: -bracketInset)
|
|
477
478
|
])
|
|
478
479
|
|
|
479
480
|
// Add observer for the end of playback
|