react-native-compressor 1.18.2 → 2.0.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.
- package/android/CMakeLists.txt +27 -0
- package/android/build.gradle +56 -58
- package/android/gradle.properties +5 -5
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/cpp/cpp-adapter.cpp +8 -0
- package/android/src/main/java/com/margelo/nitro/compressor/HybridCompressor.kt +213 -0
- package/android/src/main/java/com/reactnativecompressor/Image/ImageCompressor.kt +41 -21
- package/android/src/main/java/com/reactnativecompressor/NitroCompressorPackage.kt +30 -0
- package/android/src/main/java/com/reactnativecompressor/NitroPromiseAdapter.kt +55 -0
- package/android/src/main/java/com/reactnativecompressor/Utils/EventEmitterHandler.kt +49 -45
- package/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt +1 -1
- package/android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt +1 -1
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressionProfile.kt +21 -11
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +311 -76
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt +96 -5
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/LocationExtractor.kt +397 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/LocationBox.kt +47 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/MP4Builder.kt +39 -7
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Mp4Movie.kt +7 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/OutputSurface.kt +30 -4
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt +28 -2
- package/ios/Audio/FormatConverter/FormatConverter+Compressed.swift +1 -1
- package/ios/Audio/FormatConverter/FormatConverter+Utilities.swift +1 -1
- package/ios/Audio/FormatConverter/FormatConverter.swift +5 -3
- package/ios/HybridCompressor.swift +247 -0
- package/ios/Image/ImageCompressor.swift +8 -2
- package/ios/Utils/EventEmitterHandler.swift +56 -34
- package/ios/Video/NextLevelSessionExporter.swift +43 -8
- package/ios/Video/VideoMain.swift +44 -14
- package/lib/commonjs/Audio/index.js +1 -1
- package/lib/commonjs/Audio/index.js.map +1 -1
- package/lib/commonjs/Image/index.js +2 -22
- package/lib/commonjs/Image/index.js.map +1 -1
- package/lib/commonjs/Main.js +8 -8
- package/lib/commonjs/Main.js.map +1 -1
- package/lib/commonjs/Video/index.js +25 -65
- package/lib/commonjs/Video/index.js.map +1 -1
- package/lib/commonjs/specs/Compressor.nitro.js +6 -0
- package/lib/commonjs/specs/Compressor.nitro.js.map +1 -0
- package/lib/commonjs/utils/Downloader.js +8 -24
- package/lib/commonjs/utils/Downloader.js.map +1 -1
- package/lib/commonjs/utils/Uploader.js +3 -18
- package/lib/commonjs/utils/Uploader.js.map +1 -1
- package/lib/commonjs/utils/helpers.js +18 -1
- package/lib/commonjs/utils/helpers.js.map +1 -1
- package/lib/commonjs/utils/index.js +13 -13
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/module/Audio/index.js +2 -2
- package/lib/module/Audio/index.js.map +1 -1
- package/lib/module/Image/index.js +3 -23
- package/lib/module/Image/index.js.map +1 -1
- package/lib/module/Main.js +9 -9
- package/lib/module/Main.js.map +1 -1
- package/lib/module/Video/index.js +26 -66
- package/lib/module/Video/index.js.map +1 -1
- package/lib/module/specs/Compressor.nitro.js +4 -0
- package/lib/module/specs/Compressor.nitro.js.map +1 -0
- package/lib/module/utils/Downloader.js +10 -26
- package/lib/module/utils/Downloader.js.map +1 -1
- package/lib/module/utils/Uploader.js +5 -20
- package/lib/module/utils/Uploader.js.map +1 -1
- package/lib/module/utils/helpers.js +16 -0
- package/lib/module/utils/helpers.js.map +1 -1
- package/lib/module/utils/index.js +2 -1
- package/lib/module/utils/index.js.map +1 -1
- package/lib/typescript/src/Image/index.d.ts.map +1 -1
- package/lib/typescript/src/Main.d.ts +2 -1
- package/lib/typescript/src/Main.d.ts.map +1 -1
- package/lib/typescript/src/Video/index.d.ts +1 -1
- package/lib/typescript/src/Video/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/Compressor.nitro.d.ts +44 -0
- package/lib/typescript/src/specs/Compressor.nitro.d.ts.map +1 -0
- package/lib/typescript/src/utils/Downloader.d.ts.map +1 -1
- package/lib/typescript/src/utils/Uploader.d.ts.map +1 -1
- package/lib/typescript/src/utils/helpers.d.ts +7 -0
- package/lib/typescript/src/utils/helpers.d.ts.map +1 -1
- package/lib/typescript/src/utils/index.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroCompressor+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroCompressor+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroCompressorOnLoad.cpp +60 -0
- package/nitrogen/generated/android/NitroCompressorOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridCompressorSpec.cpp +293 -0
- package/nitrogen/generated/android/c++/JHybridCompressorSpec.hpp +78 -0
- package/nitrogen/generated/android/c++/JVideoThumbnailResult.hpp +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/HybridCompressorSpec.kt +141 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/NitroCompressorOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/VideoThumbnailResult.kt +71 -0
- package/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.hpp +204 -0
- package/nitrogen/generated/ios/react_native_compressor+autolinking.rb +62 -0
- package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.cpp +89 -0
- package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.hpp +337 -0
- package/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Umbrella.hpp +51 -0
- package/nitrogen/generated/ios/react_native_compressorAutolinking.mm +33 -0
- package/nitrogen/generated/ios/react_native_compressorAutolinking.swift +26 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_VideoThumbnailResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_AnyMap_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridCompressorSpec.swift +70 -0
- package/nitrogen/generated/ios/swift/HybridCompressorSpec_cxx.swift +493 -0
- package/nitrogen/generated/ios/swift/VideoThumbnailResult.swift +49 -0
- package/nitrogen/generated/shared/c++/HybridCompressorSpec.cpp +36 -0
- package/nitrogen/generated/shared/c++/HybridCompressorSpec.hpp +83 -0
- package/nitrogen/generated/shared/c++/VideoThumbnailResult.hpp +99 -0
- package/package.json +8 -12
- package/react-native-compressor.podspec +6 -24
- package/src/Audio/index.tsx +2 -2
- package/src/Image/index.tsx +3 -26
- package/src/Main.tsx +14 -16
- package/src/Video/index.tsx +25 -77
- package/src/specs/Compressor.nitro.ts +56 -0
- package/src/utils/Downloader.tsx +8 -27
- package/src/utils/Uploader.tsx +12 -24
- package/src/utils/helpers.ts +18 -0
- package/src/utils/index.tsx +3 -2
- package/android/src/main/java/com/reactnativecompressor/CompressorModule.kt +0 -180
- package/android/src/main/java/com/reactnativecompressor/CompressorPackage.kt +0 -34
- package/android/src/newarch/CompressorSpec.kt +0 -5
- package/android/src/oldarch/CompressorSpec.kt +0 -36
- package/ios/Compressor-Bridging-Header.h +0 -6
- package/ios/Compressor.h +0 -5
- package/ios/Compressor.mm +0 -82
- package/ios/CompressorManager.swift +0 -117
- package/lib/commonjs/Spec/NativeCompressor.js +0 -9
- package/lib/commonjs/Spec/NativeCompressor.js.map +0 -1
- package/lib/module/Spec/NativeCompressor.js +0 -5
- package/lib/module/Spec/NativeCompressor.js.map +0 -1
- package/lib/typescript/src/Spec/NativeCompressor.d.ts +0 -30
- package/lib/typescript/src/Spec/NativeCompressor.d.ts.map +0 -1
- package/src/Spec/NativeCompressor.ts +0 -41
|
@@ -49,16 +49,19 @@ object CompressorUtils {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Set up an Mp4Movie with rotation and
|
|
52
|
+
* Set up an Mp4Movie with rotation, cache file and optional ISO 6709
|
|
53
|
+
* location string forwarded from the source video.
|
|
53
54
|
*/
|
|
54
55
|
fun setUpMP4Movie(
|
|
55
56
|
rotation: Int,
|
|
56
57
|
cacheFile: File,
|
|
58
|
+
location: String? = null,
|
|
57
59
|
): Mp4Movie {
|
|
58
60
|
val movie = Mp4Movie()
|
|
59
61
|
movie.apply {
|
|
60
62
|
setCacheFile(cacheFile)
|
|
61
63
|
setRotation(rotation)
|
|
64
|
+
setLocation(location)
|
|
62
65
|
}
|
|
63
66
|
return movie
|
|
64
67
|
}
|
|
@@ -71,6 +74,7 @@ object CompressorUtils {
|
|
|
71
74
|
outputFormat: MediaFormat,
|
|
72
75
|
newBitrate: Int,
|
|
73
76
|
targetFrameRate: Int,
|
|
77
|
+
applyThroughputTuning: Boolean = true,
|
|
74
78
|
) {
|
|
75
79
|
val newFrameRate = targetFrameRate.coerceAtLeast(1)
|
|
76
80
|
val iFrameInterval = getIFrameIntervalRate(inputFormat)
|
|
@@ -84,10 +88,25 @@ object CompressorUtils {
|
|
|
84
88
|
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
|
|
85
89
|
// Bitrate in bits per second
|
|
86
90
|
setInteger(MediaFormat.KEY_BIT_RATE, newBitrate)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
|
|
92
|
+
// Throughput tuning. Some encoders reject these keys at configure() time,
|
|
93
|
+
// so the caller drops them (applyThroughputTuning = false) and reconfigures
|
|
94
|
+
// with default rate control on a fallback pass — see Compressor.prepareEncoder.
|
|
95
|
+
if (applyThroughputTuning) {
|
|
96
|
+
// VBR transcodes ~10-20% faster than CBR by skipping rate-control overhead
|
|
97
|
+
// on low-motion frames; quality stays equivalent for short-form video.
|
|
98
|
+
setInteger(
|
|
99
|
+
MediaFormat.KEY_BITRATE_MODE,
|
|
100
|
+
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR
|
|
101
|
+
)
|
|
102
|
+
// Hint the hardware codec to run as fast as it can (not throttled to
|
|
103
|
+
// realtime playback) and at the highest scheduling priority. These keys
|
|
104
|
+
// unlock full throughput on Qualcomm / Exynos / MTK SoCs that accept them.
|
|
105
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
|
106
|
+
setInteger(MediaFormat.KEY_PRIORITY, 0)
|
|
107
|
+
setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE.toInt())
|
|
108
|
+
}
|
|
109
|
+
}
|
|
91
110
|
|
|
92
111
|
getColorStandard(inputFormat)?.let {
|
|
93
112
|
setInteger(MediaFormat.KEY_COLOR_STANDARD, it)
|
|
@@ -188,4 +207,76 @@ object CompressorUtils {
|
|
|
188
207
|
}
|
|
189
208
|
return false
|
|
190
209
|
}
|
|
210
|
+
|
|
211
|
+
// MIME type reported by MediaExtractor for Dolby Vision tracks (e.g. iPhone .MOV files)
|
|
212
|
+
private const val MIMETYPE_VIDEO_DOLBY_VISION = "video/dolby-vision"
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check whether the device exposes a decoder for the given MIME type.
|
|
216
|
+
*/
|
|
217
|
+
private fun hasDecoderForMime(mime: String): Boolean {
|
|
218
|
+
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
|
219
|
+
for (codec in codecList.codecInfos) {
|
|
220
|
+
if (codec.isEncoder) continue
|
|
221
|
+
for (type in codec.supportedTypes) {
|
|
222
|
+
if (type.equals(mime, ignoreCase = true)) return true
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resolve the backward-compatible base-layer MIME type for a Dolby Vision track.
|
|
230
|
+
*
|
|
231
|
+
* Dolby Vision profiles 8.x (DvheSt) and 4 (DvheDtr) carry an HEVC base layer, and
|
|
232
|
+
* profile 9 (DvavSe) carries an AVC base layer; these can be decoded by the standard
|
|
233
|
+
* HEVC/AVC decoders. Profiles 5 (DvheStn) and 7 (DvheDtb) have no usable single base
|
|
234
|
+
* layer, so they return null. When the profile is unknown we assume HEVC, which covers
|
|
235
|
+
* the common consumer case (e.g. iPhone records Dolby Vision profile 8).
|
|
236
|
+
*/
|
|
237
|
+
private fun dolbyVisionBaseLayerMime(inputFormat: MediaFormat): String? {
|
|
238
|
+
if (!inputFormat.containsKey(MediaFormat.KEY_PROFILE)) {
|
|
239
|
+
return MediaFormat.MIMETYPE_VIDEO_HEVC
|
|
240
|
+
}
|
|
241
|
+
return when (inputFormat.getInteger(MediaFormat.KEY_PROFILE)) {
|
|
242
|
+
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvavSe -> MediaFormat.MIMETYPE_VIDEO_AVC
|
|
243
|
+
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt,
|
|
244
|
+
MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr -> MediaFormat.MIMETYPE_VIDEO_HEVC
|
|
245
|
+
else -> null
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Ensure the input video format can be decoded on this device.
|
|
251
|
+
*
|
|
252
|
+
* Some containers (notably iPhone `.MOV` files) expose the video track as
|
|
253
|
+
* `video/dolby-vision`. Many Android devices have no Dolby Vision decoder, so
|
|
254
|
+
* `MediaCodec.createDecoderByType("video/dolby-vision")` fails with NAME_NOT_FOUND.
|
|
255
|
+
* When the dedicated decoder is missing but the stream carries a backward-compatible
|
|
256
|
+
* base layer, this rewrites the format MIME to the base-layer codec so the standard
|
|
257
|
+
* HEVC/AVC decoder can decode it. If no compatible decoder exists, it throws a clear
|
|
258
|
+
* error instead of letting the cryptic native failure surface (see issue #398).
|
|
259
|
+
*/
|
|
260
|
+
fun ensureDecodableVideoFormat(inputFormat: MediaFormat) {
|
|
261
|
+
val mime = inputFormat.getString(MediaFormat.KEY_MIME) ?: return
|
|
262
|
+
if (hasDecoderForMime(mime)) return
|
|
263
|
+
|
|
264
|
+
if (mime.equals(MIMETYPE_VIDEO_DOLBY_VISION, ignoreCase = true)) {
|
|
265
|
+
val fallbackMime = dolbyVisionBaseLayerMime(inputFormat)
|
|
266
|
+
if (fallbackMime != null && hasDecoderForMime(fallbackMime)) {
|
|
267
|
+
Log.w(
|
|
268
|
+
"Compressor",
|
|
269
|
+
"No Dolby Vision decoder on this device; decoding the $fallbackMime base layer instead."
|
|
270
|
+
)
|
|
271
|
+
inputFormat.setString(MediaFormat.KEY_MIME, fallbackMime)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
throw IllegalStateException(
|
|
275
|
+
"This video uses Dolby Vision, which is not supported by this device's decoders " +
|
|
276
|
+
"and has no backward-compatible base layer to fall back to."
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
throw IllegalStateException("No decoder available for video format: $mime")
|
|
281
|
+
}
|
|
191
282
|
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
package com.reactnativecompressor.Video.VideoCompressor.utils
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import android.os.ParcelFileDescriptor
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import java.io.File
|
|
8
|
+
import java.io.FileInputStream
|
|
9
|
+
import java.nio.ByteBuffer
|
|
10
|
+
import java.nio.ByteOrder
|
|
11
|
+
import java.nio.channels.FileChannel
|
|
12
|
+
import java.nio.charset.StandardCharsets
|
|
13
|
+
import java.util.Locale
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Raw MP4 walker that recovers an ISO 6709 GPS string when
|
|
17
|
+
* MediaMetadataRetriever.METADATA_KEY_LOCATION fails to return one.
|
|
18
|
+
*
|
|
19
|
+
* Why this exists: device vendors disagree on where GPS lives.
|
|
20
|
+
* - Most phones write Apple "©xyz" under moov/udta or moov/trak/udta.
|
|
21
|
+
* - Some ISO-compliant captures use the standard "loci" box.
|
|
22
|
+
* - Newer iOS / Android captures use the iTunes-style
|
|
23
|
+
* moov/meta/keys + moov/meta/ilst pair with the key
|
|
24
|
+
* "com.apple.quicktime.location.ISO6709".
|
|
25
|
+
*
|
|
26
|
+
* Android's retriever only reads movie-level "©xyz" and silently returns
|
|
27
|
+
* null for everything else. The walker descends through every container
|
|
28
|
+
* atom and tries each known encoding in priority order.
|
|
29
|
+
*/
|
|
30
|
+
object LocationExtractor {
|
|
31
|
+
|
|
32
|
+
// Share the "Compressor" log tag so the atom dump is visible alongside
|
|
33
|
+
// the existing pipeline diagnostics without needing an extra logcat filter.
|
|
34
|
+
private const val TAG = "Compressor"
|
|
35
|
+
|
|
36
|
+
private val CONTAINER_TYPES = setOf("moov", "trak", "mdia", "minf", "udta", "meta", "ilst")
|
|
37
|
+
|
|
38
|
+
fun extract(context: Context, uri: Uri): String? {
|
|
39
|
+
Log.i(TAG, "LocationExtractor.extract uri=$uri")
|
|
40
|
+
return try {
|
|
41
|
+
openChannel(context, uri)?.use { channel ->
|
|
42
|
+
Log.i(TAG, "LocationExtractor: file size=${channel.size()}")
|
|
43
|
+
val state = WalkState()
|
|
44
|
+
walk(channel, 0L, channel.size(), state, depth = 0)
|
|
45
|
+
val viaBox = chooseBest(state)
|
|
46
|
+
// Log only presence, never the coordinate strings — these are the
|
|
47
|
+
// user's exact GPS values and must not land in production logcat.
|
|
48
|
+
Log.i(
|
|
49
|
+
TAG,
|
|
50
|
+
"LocationExtractor box scan: hasXyz=${!state.xyz.isNullOrEmpty()} hasItunesLocation=${!state.itunesLocation.isNullOrEmpty()} hasLoci=${!state.loci.isNullOrEmpty()} hasChosenLocation=${!viaBox.isNullOrEmpty()}"
|
|
51
|
+
)
|
|
52
|
+
// Samsung phones (Galaxy S10 / Android 12 verified) write GPS into
|
|
53
|
+
// an SEF (Samsung Extended Format) trailer that sits after mdat,
|
|
54
|
+
// outside the standard MP4 box hierarchy. The trailer contains
|
|
55
|
+
// an ISO 6709 string in plain ASCII. Scan the file tail and let
|
|
56
|
+
// the strict regex extract it.
|
|
57
|
+
viaBox ?: scanTrailerForIso6709(channel)
|
|
58
|
+
}
|
|
59
|
+
} catch (e: Exception) {
|
|
60
|
+
Log.w(TAG, "LocationExtractor extract failed", e)
|
|
61
|
+
null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Open a FileChannel for either a content:// URI (via ContentResolver) or
|
|
67
|
+
* a raw filesystem path. JS layer hands the compressor URIs in three
|
|
68
|
+
* shapes — content://, file://, and bare /storage/... paths — and the
|
|
69
|
+
* latter cannot be opened through ContentResolver.
|
|
70
|
+
*/
|
|
71
|
+
private fun openChannel(context: Context, uri: Uri): FileChannel? {
|
|
72
|
+
val scheme = uri.scheme
|
|
73
|
+
if (scheme == null || scheme == "file") {
|
|
74
|
+
val path = uri.path ?: uri.toString()
|
|
75
|
+
val file = File(path)
|
|
76
|
+
if (!file.exists()) {
|
|
77
|
+
Log.w(TAG, "LocationExtractor: file does not exist $path")
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
return FileInputStream(file).channel
|
|
81
|
+
}
|
|
82
|
+
val pfd = context.contentResolver.openFileDescriptor(uri, "r")
|
|
83
|
+
if (pfd == null) {
|
|
84
|
+
Log.w(TAG, "LocationExtractor: openFileDescriptor returned null for $uri")
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
// AutoCloseInputStream closes the ParcelFileDescriptor when the stream
|
|
88
|
+
// is closed. A bare FileInputStream over pfd.fileDescriptor would leak
|
|
89
|
+
// the pfd until finalizer runs.
|
|
90
|
+
return ParcelFileDescriptor.AutoCloseInputStream(pfd).channel
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Strict ISO 6709 pattern: signed lat, signed lon, optional signed alt,
|
|
94
|
+
// mandatory trailing slash. Tight enough that random bytes inside mdat
|
|
95
|
+
// virtually never match, lenient enough to accept the small precision
|
|
96
|
+
// variations vendors use.
|
|
97
|
+
private val ISO6709_REGEX = Regex(
|
|
98
|
+
"[+-]\\d{1,3}\\.\\d{2,7}[+-]\\d{1,3}\\.\\d{2,7}([+-]\\d{1,5}(\\.\\d+)?)?/"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
private fun scanTrailerForIso6709(channel: FileChannel): String? {
|
|
102
|
+
val size = channel.size()
|
|
103
|
+
// 1 MiB tail covers every SEF trailer observed so far. Capped so very
|
|
104
|
+
// small clips do not read past start of file.
|
|
105
|
+
val tailSize = minOf(size, 1L shl 20).toInt()
|
|
106
|
+
if (tailSize <= 0) return null
|
|
107
|
+
val start = size - tailSize
|
|
108
|
+
val buf = ByteBuffer.allocate(tailSize)
|
|
109
|
+
channel.position(start)
|
|
110
|
+
if (channel.read(buf) <= 0) return null
|
|
111
|
+
buf.flip()
|
|
112
|
+
val bytes = ByteArray(buf.remaining())
|
|
113
|
+
buf.get(bytes)
|
|
114
|
+
val text = String(bytes, StandardCharsets.ISO_8859_1)
|
|
115
|
+
val match = ISO6709_REGEX.find(text)
|
|
116
|
+
Log.i(TAG, "LocationExtractor SEF trailer scan matched=${match != null}")
|
|
117
|
+
return match?.value
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private class WalkState {
|
|
121
|
+
var xyz: String? = null
|
|
122
|
+
var loci: String? = null
|
|
123
|
+
var itunesLocation: String? = null
|
|
124
|
+
// iTunes-style meta state.
|
|
125
|
+
val itunesKeys: ArrayList<String> = ArrayList()
|
|
126
|
+
var insideMeta: Boolean = false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private fun chooseBest(s: WalkState): String? {
|
|
130
|
+
return s.xyz?.takeIf { it.isNotEmpty() }
|
|
131
|
+
?: s.itunesLocation?.takeIf { it.isNotEmpty() }
|
|
132
|
+
?: s.loci?.takeIf { it.isNotEmpty() }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private fun walk(
|
|
136
|
+
channel: FileChannel,
|
|
137
|
+
start: Long,
|
|
138
|
+
end: Long,
|
|
139
|
+
state: WalkState,
|
|
140
|
+
depth: Int,
|
|
141
|
+
) {
|
|
142
|
+
var pos = start
|
|
143
|
+
val header = ByteBuffer.allocate(16).order(ByteOrder.BIG_ENDIAN)
|
|
144
|
+
while (pos + 8 <= end) {
|
|
145
|
+
header.clear()
|
|
146
|
+
header.limit(8)
|
|
147
|
+
channel.position(pos)
|
|
148
|
+
if (channel.read(header) < 8) break
|
|
149
|
+
header.flip()
|
|
150
|
+
val rawSize = header.int.toLong() and 0xFFFFFFFFL
|
|
151
|
+
val typeBytes = ByteArray(4)
|
|
152
|
+
header.get(typeBytes)
|
|
153
|
+
val type = fourCC(typeBytes)
|
|
154
|
+
|
|
155
|
+
var headerSize = 8L
|
|
156
|
+
var boxSize = rawSize
|
|
157
|
+
if (rawSize == 1L) {
|
|
158
|
+
val ext = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN)
|
|
159
|
+
channel.position(pos + 8)
|
|
160
|
+
if (channel.read(ext) < 8) break
|
|
161
|
+
ext.flip()
|
|
162
|
+
boxSize = ext.long
|
|
163
|
+
headerSize = 16L
|
|
164
|
+
} else if (rawSize == 0L) {
|
|
165
|
+
boxSize = end - pos
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (boxSize < headerSize || pos + boxSize > end) break
|
|
169
|
+
val childEnd = pos + boxSize
|
|
170
|
+
val childStart = pos + headerSize
|
|
171
|
+
|
|
172
|
+
if (depth < 5) {
|
|
173
|
+
// Use Log.i so the atom tree appears in default logcat output and
|
|
174
|
+
// can be pasted back when GPS extraction misses a vendor-specific
|
|
175
|
+
// box layout. Depth-bounded to avoid spamming on large mdat chunks.
|
|
176
|
+
Log.i(TAG, "LocationExtractor atom $type @ $pos size=$boxSize depth=$depth")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
when {
|
|
180
|
+
// Apple Quicktime "©xyz" - 0xA9 'x' 'y' 'z'.
|
|
181
|
+
typeBytes[0] == 0xA9.toByte() &&
|
|
182
|
+
typeBytes[1] == 'x'.code.toByte() &&
|
|
183
|
+
typeBytes[2] == 'y'.code.toByte() &&
|
|
184
|
+
typeBytes[3] == 'z'.code.toByte() -> {
|
|
185
|
+
val parsed = readXyz(channel, childStart, childEnd)
|
|
186
|
+
if (!parsed.isNullOrEmpty() && state.xyz == null) {
|
|
187
|
+
state.xyz = parsed
|
|
188
|
+
Log.i(TAG, "found ©xyz")
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ISO 14496-12 "loci" location box.
|
|
193
|
+
type == "loci" -> {
|
|
194
|
+
val parsed = readLoci(channel, childStart, childEnd)
|
|
195
|
+
if (!parsed.isNullOrEmpty() && state.loci == null) {
|
|
196
|
+
state.loci = parsed
|
|
197
|
+
Log.i(TAG, "found loci")
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// iTunes-style metadata under moov/meta.
|
|
202
|
+
type == "keys" && state.insideMeta -> {
|
|
203
|
+
parseItunesKeys(channel, childStart, childEnd, state)
|
|
204
|
+
}
|
|
205
|
+
type == "ilst" && state.insideMeta -> {
|
|
206
|
+
parseItunesIlst(channel, childStart, childEnd, state)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (type in CONTAINER_TYPES) {
|
|
211
|
+
// "meta" has a 4-byte version+flags prefix before its children.
|
|
212
|
+
val innerStart = if (type == "meta") childStart + 4 else childStart
|
|
213
|
+
val priorMeta = state.insideMeta
|
|
214
|
+
if (type == "meta") state.insideMeta = true
|
|
215
|
+
walk(channel, innerStart, childEnd, state, depth + 1)
|
|
216
|
+
state.insideMeta = priorMeta
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
pos = childEnd
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private fun fourCC(b: ByteArray): String {
|
|
224
|
+
val sb = StringBuilder(4)
|
|
225
|
+
for (byte in b) {
|
|
226
|
+
val c = byte.toInt() and 0xFF
|
|
227
|
+
sb.append(if (c in 0x20..0x7E) c.toChar() else '?')
|
|
228
|
+
}
|
|
229
|
+
return sb.toString()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private fun readBoxContent(channel: FileChannel, start: Long, end: Long): ByteBuffer? {
|
|
233
|
+
val len = (end - start).toInt()
|
|
234
|
+
if (len <= 0) return null
|
|
235
|
+
val buf = ByteBuffer.allocate(len).order(ByteOrder.BIG_ENDIAN)
|
|
236
|
+
channel.position(start)
|
|
237
|
+
if (channel.read(buf) < len) return null
|
|
238
|
+
buf.flip()
|
|
239
|
+
return buf
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Apple "©xyz" content:
|
|
244
|
+
* uint16 length
|
|
245
|
+
* uint16 language code (packed)
|
|
246
|
+
* bytes ISO 6709 string
|
|
247
|
+
*/
|
|
248
|
+
private fun readXyz(channel: FileChannel, start: Long, end: Long): String? {
|
|
249
|
+
val buf = readBoxContent(channel, start, end) ?: return null
|
|
250
|
+
if (buf.remaining() < 4) return null
|
|
251
|
+
val len = buf.short.toInt() and 0xFFFF
|
|
252
|
+
buf.short
|
|
253
|
+
val take = minOf(len, buf.remaining())
|
|
254
|
+
if (take <= 0) return null
|
|
255
|
+
val bytes = ByteArray(take)
|
|
256
|
+
buf.get(bytes)
|
|
257
|
+
return String(bytes, StandardCharsets.UTF_8).trim().ifEmpty { null }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* ISO 14496-12 "loci" content:
|
|
262
|
+
* uint8 version
|
|
263
|
+
* uint24 flags
|
|
264
|
+
* uint16 language
|
|
265
|
+
* utf8z name
|
|
266
|
+
* uint8 role
|
|
267
|
+
* uint32 longitude (16.16 fixed)
|
|
268
|
+
* uint32 latitude (16.16 fixed)
|
|
269
|
+
* uint32 altitude (16.16 fixed)
|
|
270
|
+
* ...
|
|
271
|
+
*/
|
|
272
|
+
private fun readLoci(channel: FileChannel, start: Long, end: Long): String? {
|
|
273
|
+
val buf = readBoxContent(channel, start, end) ?: return null
|
|
274
|
+
if (buf.remaining() < 6) return null
|
|
275
|
+
buf.int // version + flags
|
|
276
|
+
buf.short // language
|
|
277
|
+
// Skip null-terminated name.
|
|
278
|
+
while (buf.hasRemaining() && buf.get() != 0.toByte()) { /* skip */ }
|
|
279
|
+
if (buf.remaining() < 1 + 12) return null
|
|
280
|
+
buf.get() // role
|
|
281
|
+
val longitude = fixedPoint1616(buf.int)
|
|
282
|
+
val latitude = fixedPoint1616(buf.int)
|
|
283
|
+
val altitude = fixedPoint1616(buf.int)
|
|
284
|
+
return formatIso6709(latitude, longitude, altitude)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private fun fixedPoint1616(raw: Int): Double {
|
|
288
|
+
return raw.toDouble() / 65536.0
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private fun formatIso6709(lat: Double, lon: Double, alt: Double): String {
|
|
292
|
+
val sb = StringBuilder()
|
|
293
|
+
sb.append(if (lat >= 0) "+" else "")
|
|
294
|
+
sb.append(String.format(Locale.US, "%.4f", lat))
|
|
295
|
+
sb.append(if (lon >= 0) "+" else "")
|
|
296
|
+
sb.append(String.format(Locale.US, "%.4f", lon))
|
|
297
|
+
if (alt != 0.0) {
|
|
298
|
+
sb.append(if (alt >= 0) "+" else "")
|
|
299
|
+
sb.append(String.format(Locale.US, "%.3f", alt))
|
|
300
|
+
}
|
|
301
|
+
sb.append('/')
|
|
302
|
+
return sb.toString()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Apple iTunes-style "keys" box content:
|
|
307
|
+
* uint32 version+flags
|
|
308
|
+
* uint32 entry_count
|
|
309
|
+
* for each:
|
|
310
|
+
* uint32 key_size (includes header)
|
|
311
|
+
* uint32 key_namespace ('mdta')
|
|
312
|
+
* bytes key_value (utf-8)
|
|
313
|
+
*/
|
|
314
|
+
private fun parseItunesKeys(channel: FileChannel, start: Long, end: Long, state: WalkState) {
|
|
315
|
+
val buf = readBoxContent(channel, start, end) ?: return
|
|
316
|
+
state.itunesKeys.clear()
|
|
317
|
+
if (buf.remaining() < 8) return
|
|
318
|
+
buf.int // version + flags
|
|
319
|
+
val count = buf.int
|
|
320
|
+
for (i in 0 until count) {
|
|
321
|
+
if (buf.remaining() < 8) break
|
|
322
|
+
val entrySize = buf.int
|
|
323
|
+
buf.int // namespace
|
|
324
|
+
val keyLen = entrySize - 8
|
|
325
|
+
if (keyLen <= 0 || keyLen > buf.remaining()) break
|
|
326
|
+
val keyBytes = ByteArray(keyLen)
|
|
327
|
+
buf.get(keyBytes)
|
|
328
|
+
state.itunesKeys.add(String(keyBytes, StandardCharsets.UTF_8))
|
|
329
|
+
}
|
|
330
|
+
Log.i(TAG, "LocationExtractor itunes keys: ${state.itunesKeys}")
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Apple iTunes-style "ilst" box. Each child is an indexed item whose
|
|
335
|
+
* type is a uint32 index pointing back into the "keys" table. Inside
|
|
336
|
+
* each item is a "data" sub-box with the actual payload.
|
|
337
|
+
*/
|
|
338
|
+
private fun parseItunesIlst(channel: FileChannel, start: Long, end: Long, state: WalkState) {
|
|
339
|
+
var pos = start
|
|
340
|
+
val header = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN)
|
|
341
|
+
while (pos + 8 <= end) {
|
|
342
|
+
header.clear()
|
|
343
|
+
channel.position(pos)
|
|
344
|
+
if (channel.read(header) < 8) break
|
|
345
|
+
header.flip()
|
|
346
|
+
val itemSize = header.int.toLong() and 0xFFFFFFFFL
|
|
347
|
+
val indexBytes = ByteArray(4)
|
|
348
|
+
header.get(indexBytes)
|
|
349
|
+
val index = ByteBuffer.wrap(indexBytes).order(ByteOrder.BIG_ENDIAN).int
|
|
350
|
+
if (itemSize < 8 || pos + itemSize > end) break
|
|
351
|
+
val itemEnd = pos + itemSize
|
|
352
|
+
val key = state.itunesKeys.getOrNull(index - 1)
|
|
353
|
+
if (key == "com.apple.quicktime.location.ISO6709") {
|
|
354
|
+
val payload = findItunesData(channel, pos + 8, itemEnd)
|
|
355
|
+
if (!payload.isNullOrEmpty() && state.itunesLocation == null) {
|
|
356
|
+
state.itunesLocation = payload
|
|
357
|
+
Log.i(TAG, "found itunes location")
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
pos = itemEnd
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private fun findItunesData(channel: FileChannel, start: Long, end: Long): String? {
|
|
365
|
+
var pos = start
|
|
366
|
+
val header = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN)
|
|
367
|
+
while (pos + 8 <= end) {
|
|
368
|
+
header.clear()
|
|
369
|
+
channel.position(pos)
|
|
370
|
+
if (channel.read(header) < 8) break
|
|
371
|
+
header.flip()
|
|
372
|
+
val size = header.int.toLong() and 0xFFFFFFFFL
|
|
373
|
+
val typeBytes = ByteArray(4)
|
|
374
|
+
header.get(typeBytes)
|
|
375
|
+
val type = fourCC(typeBytes)
|
|
376
|
+
if (size < 8 || pos + size > end) break
|
|
377
|
+
if (type == "data") {
|
|
378
|
+
// data box: uint32 type indicator, uint32 locale, then payload.
|
|
379
|
+
val payloadStart = pos + 8 + 8
|
|
380
|
+
val payloadEnd = pos + size
|
|
381
|
+
if (payloadEnd > payloadStart) {
|
|
382
|
+
val len = (payloadEnd - payloadStart).toInt()
|
|
383
|
+
val buf = ByteBuffer.allocate(len)
|
|
384
|
+
channel.position(payloadStart)
|
|
385
|
+
if (channel.read(buf) >= len) {
|
|
386
|
+
buf.flip()
|
|
387
|
+
val bytes = ByteArray(len)
|
|
388
|
+
buf.get(bytes)
|
|
389
|
+
return String(bytes, StandardCharsets.UTF_8).trim().ifEmpty { null }
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
pos += size
|
|
394
|
+
}
|
|
395
|
+
return null
|
|
396
|
+
}
|
|
397
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package com.reactnativecompressor.Video.VideoCompressor.video
|
|
2
|
+
|
|
3
|
+
import org.mp4parser.support.AbstractBox
|
|
4
|
+
import java.nio.ByteBuffer
|
|
5
|
+
import java.nio.charset.StandardCharsets
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Apple Quicktime "©xyz" box that stores an ISO 6709 location string
|
|
9
|
+
* (e.g. "+37.4220-122.0840/" or "+37.4220-122.0840+009.000/").
|
|
10
|
+
*
|
|
11
|
+
* Android's MediaMetadataRetriever.METADATA_KEY_LOCATION reads this exact
|
|
12
|
+
* box; writing it preserves GPS metadata across the compression rewrite.
|
|
13
|
+
*
|
|
14
|
+
* Layout of the box content:
|
|
15
|
+
* uint16 BE text byte length
|
|
16
|
+
* uint16 BE language packed code (0x15c7 = "und")
|
|
17
|
+
* bytes ISO 6709 string (no NUL terminator)
|
|
18
|
+
*/
|
|
19
|
+
class LocationBox : AbstractBox(TYPE) {
|
|
20
|
+
|
|
21
|
+
var location: String = ""
|
|
22
|
+
|
|
23
|
+
override fun getContentSize(): Long {
|
|
24
|
+
val bytes = location.toByteArray(StandardCharsets.UTF_8)
|
|
25
|
+
return (2 + 2 + bytes.size).toLong()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun _parseDetails(content: ByteBuffer) {
|
|
29
|
+
val len = content.short.toInt() and 0xFFFF
|
|
30
|
+
content.short
|
|
31
|
+
val bytes = ByteArray(len)
|
|
32
|
+
content.get(bytes)
|
|
33
|
+
location = String(bytes, StandardCharsets.UTF_8)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun getContent(byteBuffer: ByteBuffer) {
|
|
37
|
+
val bytes = location.toByteArray(StandardCharsets.UTF_8)
|
|
38
|
+
byteBuffer.putShort(bytes.size.toShort())
|
|
39
|
+
byteBuffer.putShort(LANG_UND)
|
|
40
|
+
byteBuffer.put(bytes)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
companion object {
|
|
44
|
+
const val TYPE = "©xyz"
|
|
45
|
+
private const val LANG_UND: Short = 0x15c7
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -30,13 +30,20 @@ class MP4Builder {
|
|
|
30
30
|
fos = FileOutputStream(mp4Movie.getCacheFile())
|
|
31
31
|
fc = fos.channel
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
// Streams are open now; if header writing throws, the caller never gets
|
|
34
|
+
// a reference to close, so release them here before rethrowing.
|
|
35
|
+
try {
|
|
36
|
+
val fileTypeBox: FileTypeBox = createFileTypeBox()
|
|
37
|
+
fileTypeBox.getBox(fc)
|
|
38
|
+
dataOffset += fileTypeBox.size
|
|
39
|
+
wroteSinceLastMdat = dataOffset
|
|
40
|
+
|
|
41
|
+
mdat = Mdat()
|
|
42
|
+
sizeBuffer = ByteBuffer.allocateDirect(4)
|
|
43
|
+
} catch (e: Exception) {
|
|
44
|
+
close()
|
|
45
|
+
throw e
|
|
46
|
+
}
|
|
40
47
|
|
|
41
48
|
return this
|
|
42
49
|
}
|
|
@@ -131,6 +138,19 @@ class MP4Builder {
|
|
|
131
138
|
fos.close()
|
|
132
139
|
}
|
|
133
140
|
|
|
141
|
+
// Close the underlying file streams without finalizing the movie. Used on
|
|
142
|
+
// failure paths where finishMovie() never runs, so the FileOutputStream and
|
|
143
|
+
// its FileChannel opened in createMovie() don't leak. Safe to call when
|
|
144
|
+
// createMovie() failed early (streams not yet initialized) and idempotent.
|
|
145
|
+
fun close() {
|
|
146
|
+
if (::fc.isInitialized) {
|
|
147
|
+
runCatching { fc.close() }
|
|
148
|
+
}
|
|
149
|
+
if (::fos.isInitialized) {
|
|
150
|
+
runCatching { fos.close() }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
134
154
|
private fun createFileTypeBox(): FileTypeBox {
|
|
135
155
|
// completed list can be found at https://www.ftyps.com/
|
|
136
156
|
val minorBrands = listOf(
|
|
@@ -190,6 +210,18 @@ class MP4Builder {
|
|
|
190
210
|
movieBox.addBox(createTrackBox(track, movie))
|
|
191
211
|
}
|
|
192
212
|
|
|
213
|
+
// Preserve source GPS metadata. MediaMetadataRetriever and most
|
|
214
|
+
// gallery apps read the Apple "©xyz" box inside moov/udta, so any
|
|
215
|
+
// ISO 6709 string passed in is written there verbatim.
|
|
216
|
+
val location = movie.getLocation()
|
|
217
|
+
if (!location.isNullOrEmpty()) {
|
|
218
|
+
val udta = UserDataBox()
|
|
219
|
+
val xyz = LocationBox()
|
|
220
|
+
xyz.location = location
|
|
221
|
+
udta.addBox(xyz)
|
|
222
|
+
movieBox.addBox(udta)
|
|
223
|
+
}
|
|
224
|
+
|
|
193
225
|
return movieBox
|
|
194
226
|
}
|
|
195
227
|
|
|
@@ -11,6 +11,7 @@ class Mp4Movie {
|
|
|
11
11
|
private var matrix = Matrix.ROTATE_0
|
|
12
12
|
private val tracks = ArrayList<Track>()
|
|
13
13
|
private var cacheFile: File? = null
|
|
14
|
+
private var location: String? = null
|
|
14
15
|
|
|
15
16
|
fun getMatrix(): Matrix? = matrix
|
|
16
17
|
|
|
@@ -18,6 +19,12 @@ class Mp4Movie {
|
|
|
18
19
|
cacheFile = file
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
fun setLocation(value: String?) {
|
|
23
|
+
location = value
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fun getLocation(): String? = location
|
|
27
|
+
|
|
21
28
|
fun setRotation(angle: Int) {
|
|
22
29
|
when (angle) {
|
|
23
30
|
0 -> {
|