react-native-compressor 1.19.2 → 1.19.4
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 +7 -1
- package/android/build.gradle +0 -1
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt +152 -139
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioTranscoder.kt +204 -0
- package/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +401 -379
- package/ios/Image/ImageCompressor.swift +37 -16
- package/ios/Image/ImageMain.swift +21 -11
- package/package.json +1 -1
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt +0 -112
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<img height="150" src="/media/logo.png" />
|
|
3
3
|
</div>
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
<br/>
|
|
6
6
|
|
|
7
7
|
<div align="center">
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
|
+
> ⚠️ **Legacy Notice**
|
|
18
|
+
>
|
|
19
|
+
> This repository is now in **maintenance mode** for the v1 series. Version **1.19.3** will be the last release of v1. Development has moved to **[v2](https://github.com/numandev1/react-native-compressor)** which uses **Nitro Modules** for improved performance and maintainability.
|
|
20
|
+
>
|
|
21
|
+
> New features, bug fixes and enhancements will land in v2 only.
|
|
22
|
+
|
|
17
23
|
**REACT-NATIVE-COMPRESSOR** is a react-native package, which helps us to Compress `Image`, `Video`, and `Audio` before uploading, same like **Whatsapp** without knowing the compression `algorithm`
|
|
18
24
|
|
|
19
25
|
<div align="center">
|
package/android/build.gradle
CHANGED
|
@@ -119,7 +119,6 @@ dependencies {
|
|
|
119
119
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
|
|
120
120
|
implementation 'org.mp4parser:isoparser:1.9.56'
|
|
121
121
|
implementation 'com.github.kaushik-naik:TAndroidLame:277c2ab4b0'
|
|
122
|
-
implementation 'javazoom:jlayer:1.0.1'
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
if (isNewArchitectureEnabled()) {
|
|
@@ -10,8 +10,6 @@ import com.naman14.androidlame.WaveReader
|
|
|
10
10
|
import com.reactnativecompressor.Utils.MediaCache
|
|
11
11
|
import com.reactnativecompressor.Utils.Utils
|
|
12
12
|
import com.reactnativecompressor.Utils.Utils.addLog
|
|
13
|
-
import javazoom.jl.converter.Converter
|
|
14
|
-
import javazoom.jl.decoder.JavaLayerException
|
|
15
13
|
import java.io.BufferedOutputStream
|
|
16
14
|
import java.io.File
|
|
17
15
|
import java.io.FileNotFoundException
|
|
@@ -20,11 +18,12 @@ import java.io.IOException
|
|
|
20
18
|
|
|
21
19
|
class AudioCompressor {
|
|
22
20
|
companion object {
|
|
23
|
-
val TAG="AudioMain"
|
|
21
|
+
val TAG = "AudioMain"
|
|
24
22
|
private const val OUTPUT_STREAM_BUFFER = 8192
|
|
25
23
|
|
|
26
24
|
var outputStream: BufferedOutputStream? = null
|
|
27
25
|
var waveReader: WaveReader? = null
|
|
26
|
+
|
|
28
27
|
@JvmStatic
|
|
29
28
|
fun CompressAudio(
|
|
30
29
|
fileUrl: String,
|
|
@@ -33,59 +32,76 @@ class AudioCompressor {
|
|
|
33
32
|
promise: Promise,
|
|
34
33
|
) {
|
|
35
34
|
val realPath = Utils.getRealPath(fileUrl, context)
|
|
36
|
-
var _fileUrl=realPath
|
|
35
|
+
var _fileUrl = realPath
|
|
37
36
|
val filePathWithoutFileUri = realPath!!.replace("file://", "")
|
|
37
|
+
|
|
38
|
+
// These are declared outside the try block so they can be cleaned up
|
|
39
|
+
// in the catch handler. They must be nullable because the transcoding
|
|
40
|
+
// branch may not execute.
|
|
41
|
+
var wavPath: String? = null
|
|
42
|
+
var isNonWav: Boolean = false
|
|
43
|
+
|
|
38
44
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
wavPath = filePathWithoutFileUri
|
|
46
|
+
// LAME's WaveReader requires PCM 16-bit LE WAV. Anything else
|
|
47
|
+
// (m4a/AAC, mp3, ogg, flac, video audio tracks, ...) must be
|
|
48
|
+
// decoded to WAV first via the platform MediaCodec pipeline —
|
|
49
|
+
// jlayer's MP3-only Converter silently produced empty WAVs for
|
|
50
|
+
// every non-MP3 input, which LAME then encoded as a few seconds
|
|
51
|
+
// of silence (see #410 thread, m4a/AAC support).
|
|
52
|
+
//
|
|
53
|
+
// Check the resolved local path, not the original fileUrl: a
|
|
54
|
+
// content:// or remote URL won't carry the file extension, so
|
|
55
|
+
// checking fileUrl would mis-classify WAV inputs as non-WAV and
|
|
56
|
+
// route them through an unnecessary (and potentially failing)
|
|
57
|
+
// MediaCodec decode pass.
|
|
58
|
+
if (!filePathWithoutFileUri.endsWith(".wav", ignoreCase = true)) {
|
|
59
|
+
addLog("non wav file found, transcoding to PCM WAV")
|
|
60
|
+
wavPath = Utils.generateCacheFilePath("wav", context)
|
|
48
61
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
AudioTranscoder.decodeToWav(filePathWithoutFileUri, wavPath)
|
|
63
|
+
} catch (e: IOException) {
|
|
64
|
+
addLog("AudioTranscoder failed: " + e.localizedMessage)
|
|
65
|
+
// Surface the failure rather than silently returning a
|
|
66
|
+
// degenerate MP3 of silence.
|
|
67
|
+
File(wavPath).delete()
|
|
68
|
+
promise.reject("audio_transcode_failed", e.localizedMessage ?: "Audio decode failed", e)
|
|
69
|
+
return
|
|
54
70
|
}
|
|
55
|
-
isNonWav=true
|
|
56
|
-
}
|
|
57
|
-
else if (!fileUrl.endsWith(".wav", ignoreCase = true))
|
|
58
|
-
{
|
|
59
|
-
addLog("non wav file found")
|
|
60
|
-
wavPath= Utils.generateCacheFilePath("wav", context)
|
|
61
|
-
try {
|
|
62
|
-
val converter = Converter()
|
|
63
|
-
converter.convert(filePathWithoutFileUri, wavPath)
|
|
64
|
-
} catch (e: JavaLayerException) {
|
|
65
|
-
addLog("JavaLayerException error"+e.localizedMessage)
|
|
66
|
-
e.printStackTrace();
|
|
67
|
-
}
|
|
68
|
-
isNonWav=true
|
|
71
|
+
isNonWav = true
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
autoCompressHelper(wavPath,filePathWithoutFileUri, optionMap,context) { mp3Path, finished ->
|
|
74
|
+
autoCompressHelper(wavPath, filePathWithoutFileUri, optionMap, context) { mp3Path, finished ->
|
|
73
75
|
if (finished) {
|
|
74
|
-
val returnableFilePath:String="file://$mp3Path"
|
|
76
|
+
val returnableFilePath: String = "file://$mp3Path"
|
|
75
77
|
addLog("finished: " + returnableFilePath)
|
|
76
78
|
MediaCache.removeCompletedImagePath(fileUrl)
|
|
77
|
-
if(isNonWav)
|
|
78
|
-
{
|
|
79
|
+
if (isNonWav) {
|
|
79
80
|
File(wavPath).delete()
|
|
80
81
|
}
|
|
81
82
|
promise.resolve(returnableFilePath)
|
|
82
83
|
} else {
|
|
83
|
-
addLog("error: "+mp3Path)
|
|
84
|
+
addLog("error: " + mp3Path)
|
|
85
|
+
// Clean up the temp WAV on failure too; previously it was
|
|
86
|
+
// leaked because only the success path deleted it.
|
|
87
|
+
if (isNonWav) {
|
|
88
|
+
File(wavPath).delete()
|
|
89
|
+
}
|
|
84
90
|
promise.resolve(_fileUrl)
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
93
|
} catch (e: Exception) {
|
|
88
|
-
|
|
94
|
+
addLog("CompressAudio failed: " + e.localizedMessage)
|
|
95
|
+
// Clean up any temp WAV that was created before the failure.
|
|
96
|
+
val temp = wavPath
|
|
97
|
+
if (isNonWav && temp != null) {
|
|
98
|
+
try {
|
|
99
|
+
File(temp).delete()
|
|
100
|
+
} catch (_: Throwable) {
|
|
101
|
+
// ignore cleanup failures
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
promise.reject("audio_compress_failed", e.localizedMessage ?: "Audio compression failed", e)
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
107
|
|
|
@@ -101,35 +117,34 @@ class AudioCompressor {
|
|
|
101
117
|
val options = AudioHelper.fromMap(optionMap)
|
|
102
118
|
val quality = options.quality
|
|
103
119
|
|
|
104
|
-
var isCompletedCallbackTriggered:Boolean=false
|
|
120
|
+
var isCompletedCallbackTriggered: Boolean = false
|
|
105
121
|
try {
|
|
106
122
|
var mp3Path = Utils.generateCacheFilePath("mp3", context)
|
|
107
123
|
val input = File(fileUrl)
|
|
108
124
|
val output = File(mp3Path)
|
|
109
125
|
|
|
110
126
|
val CHUNK_SIZE = 8192
|
|
111
|
-
|
|
127
|
+
addLog("Initialising wav reader")
|
|
112
128
|
|
|
113
|
-
|
|
129
|
+
waveReader = WaveReader(input)
|
|
114
130
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
try {
|
|
132
|
+
waveReader!!.openWave()
|
|
133
|
+
} catch (e: IOException) {
|
|
134
|
+
addLog("openWave failed: " + e.localizedMessage)
|
|
135
|
+
completeCallback(e.localizedMessage ?: "Failed to open WAV", false)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
120
138
|
|
|
121
|
-
|
|
139
|
+
addLog("Intitialising encoder")
|
|
122
140
|
|
|
123
141
|
|
|
124
142
|
// for bitrate
|
|
125
|
-
var audioBitrate:Int
|
|
126
|
-
if(options.bitrate != -1)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
else
|
|
131
|
-
{
|
|
132
|
-
audioBitrate=AudioHelper.getDestinationBitrateByQuality(actualFileUrl, quality!!)
|
|
143
|
+
var audioBitrate: Int
|
|
144
|
+
if (options.bitrate != -1) {
|
|
145
|
+
audioBitrate = options.bitrate / 1000
|
|
146
|
+
} else {
|
|
147
|
+
audioBitrate = AudioHelper.getDestinationBitrateByQuality(actualFileUrl, quality!!)
|
|
133
148
|
Utils.addLog("dest bitrate: $audioBitrate")
|
|
134
149
|
}
|
|
135
150
|
|
|
@@ -137,128 +152,126 @@ class AudioCompressor {
|
|
|
137
152
|
androidLame.setOutBitrate(audioBitrate)
|
|
138
153
|
|
|
139
154
|
// for channels
|
|
140
|
-
var audioChannels:Int
|
|
141
|
-
if(options.channels != -1){
|
|
142
|
-
audioChannels= options.channels!!
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
{
|
|
146
|
-
audioChannels=waveReader!!.channels
|
|
155
|
+
var audioChannels: Int
|
|
156
|
+
if (options.channels != -1) {
|
|
157
|
+
audioChannels = options.channels!!
|
|
158
|
+
} else {
|
|
159
|
+
audioChannels = waveReader!!.channels
|
|
147
160
|
}
|
|
148
161
|
androidLame.setOutChannels(audioChannels)
|
|
149
162
|
|
|
150
163
|
// for sample rate
|
|
151
164
|
androidLame.setInSampleRate(waveReader!!.sampleRate)
|
|
152
|
-
var audioSampleRate:Int
|
|
153
|
-
if(options.samplerate != -1){
|
|
154
|
-
audioSampleRate= options.samplerate!!
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
{
|
|
158
|
-
audioSampleRate=waveReader!!.sampleRate
|
|
165
|
+
var audioSampleRate: Int
|
|
166
|
+
if (options.samplerate != -1) {
|
|
167
|
+
audioSampleRate = options.samplerate!!
|
|
168
|
+
} else {
|
|
169
|
+
audioSampleRate = waveReader!!.sampleRate
|
|
159
170
|
}
|
|
160
171
|
androidLame.setOutSampleRate(audioSampleRate)
|
|
161
|
-
val androidLameBuild=androidLame.build()
|
|
172
|
+
val androidLameBuild = androidLame.build()
|
|
162
173
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
try {
|
|
175
|
+
outputStream = BufferedOutputStream(FileOutputStream(output), OUTPUT_STREAM_BUFFER)
|
|
176
|
+
} catch (e: FileNotFoundException) {
|
|
177
|
+
addLog("Failed to create output stream: " + e.localizedMessage)
|
|
178
|
+
completeCallback(e.localizedMessage ?: "Failed to create output file", false)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
168
181
|
|
|
169
|
-
|
|
182
|
+
var bytesRead = 0
|
|
170
183
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
val buffer_l = ShortArray(CHUNK_SIZE)
|
|
185
|
+
val buffer_r = ShortArray(CHUNK_SIZE)
|
|
186
|
+
val mp3Buf = ByteArray(CHUNK_SIZE)
|
|
174
187
|
|
|
175
|
-
|
|
188
|
+
val channels = waveReader!!.channels
|
|
176
189
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
190
|
+
addLog("started encoding")
|
|
191
|
+
while (true) {
|
|
192
|
+
try {
|
|
193
|
+
if (channels == 2) {
|
|
194
|
+
|
|
195
|
+
bytesRead = waveReader!!.read(buffer_l, buffer_r, CHUNK_SIZE)
|
|
196
|
+
addLog("bytes read=$bytesRead")
|
|
181
197
|
|
|
182
|
-
|
|
183
|
-
addLog("bytes read=$bytesRead")
|
|
198
|
+
if (bytesRead > 0) {
|
|
184
199
|
|
|
185
|
-
|
|
200
|
+
var bytesEncoded = 0
|
|
201
|
+
bytesEncoded = androidLameBuild.encode(buffer_l, buffer_r, bytesRead, mp3Buf)
|
|
202
|
+
addLog("bytes encoded=$bytesEncoded")
|
|
186
203
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
204
|
+
if (bytesEncoded > 0) {
|
|
205
|
+
try {
|
|
206
|
+
addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
|
|
207
|
+
outputStream!!.write(mp3Buf, 0, bytesEncoded)
|
|
208
|
+
} catch (e: IOException) {
|
|
209
|
+
e.printStackTrace()
|
|
210
|
+
}
|
|
190
211
|
|
|
191
|
-
if (bytesEncoded > 0) {
|
|
192
|
-
try {
|
|
193
|
-
addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
|
|
194
|
-
outputStream!!.write(mp3Buf, 0, bytesEncoded)
|
|
195
|
-
} catch (e: IOException) {
|
|
196
|
-
e.printStackTrace()
|
|
197
212
|
}
|
|
198
213
|
|
|
199
|
-
}
|
|
214
|
+
} else
|
|
215
|
+
break
|
|
216
|
+
} else {
|
|
200
217
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
} else {
|
|
218
|
+
bytesRead = waveReader!!.read(buffer_l, CHUNK_SIZE)
|
|
219
|
+
addLog("bytes read=$bytesRead")
|
|
204
220
|
|
|
205
|
-
|
|
206
|
-
|
|
221
|
+
if (bytesRead > 0) {
|
|
222
|
+
var bytesEncoded = 0
|
|
207
223
|
|
|
208
|
-
|
|
209
|
-
|
|
224
|
+
bytesEncoded = androidLameBuild.encode(buffer_l, buffer_l, bytesRead, mp3Buf)
|
|
225
|
+
addLog("bytes encoded=$bytesEncoded")
|
|
210
226
|
|
|
211
|
-
|
|
212
|
-
|
|
227
|
+
if (bytesEncoded > 0) {
|
|
228
|
+
try {
|
|
229
|
+
addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
|
|
230
|
+
outputStream!!.write(mp3Buf, 0, bytesEncoded)
|
|
231
|
+
} catch (e: IOException) {
|
|
232
|
+
e.printStackTrace()
|
|
233
|
+
}
|
|
213
234
|
|
|
214
|
-
if (bytesEncoded > 0) {
|
|
215
|
-
try {
|
|
216
|
-
addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
|
|
217
|
-
outputStream!!.write(mp3Buf, 0, bytesEncoded)
|
|
218
|
-
} catch (e: IOException) {
|
|
219
|
-
e.printStackTrace()
|
|
220
235
|
}
|
|
221
236
|
|
|
222
|
-
}
|
|
237
|
+
} else
|
|
238
|
+
break
|
|
239
|
+
}
|
|
223
240
|
|
|
224
|
-
} else
|
|
225
|
-
break
|
|
226
|
-
}
|
|
227
241
|
|
|
242
|
+
} catch (e: IOException) {
|
|
243
|
+
e.printStackTrace()
|
|
244
|
+
}
|
|
228
245
|
|
|
229
|
-
} catch (e: IOException) {
|
|
230
|
-
e.printStackTrace()
|
|
231
246
|
}
|
|
232
247
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
addLog("flushing final mp3buffer")
|
|
249
|
+
val outputMp3buf = androidLameBuild.flush(mp3Buf)
|
|
250
|
+
addLog("flushed $outputMp3buf bytes")
|
|
251
|
+
if (outputMp3buf > 0) {
|
|
252
|
+
try {
|
|
253
|
+
addLog("writing final mp3buffer to outputstream")
|
|
254
|
+
outputStream!!.write(mp3Buf, 0, outputMp3buf)
|
|
255
|
+
addLog("closing output stream")
|
|
256
|
+
outputStream!!.close()
|
|
257
|
+
completeCallback(output.absolutePath, true)
|
|
258
|
+
isCompletedCallbackTriggered = true
|
|
259
|
+
} catch (e: IOException) {
|
|
260
|
+
completeCallback(e.localizedMessage, false)
|
|
261
|
+
isCompletedCallbackTriggered = true
|
|
262
|
+
e.printStackTrace()
|
|
263
|
+
}
|
|
249
264
|
}
|
|
250
|
-
}
|
|
251
265
|
|
|
252
266
|
} catch (e: IOException) {
|
|
253
267
|
completeCallback(e.localizedMessage, false)
|
|
268
|
+
isCompletedCallbackTriggered = true
|
|
254
269
|
}
|
|
255
|
-
if(!isCompletedCallbackTriggered)
|
|
256
|
-
{
|
|
270
|
+
if (!isCompletedCallbackTriggered) {
|
|
257
271
|
completeCallback("something went wrong", false)
|
|
258
272
|
}
|
|
259
273
|
}
|
|
260
274
|
|
|
261
275
|
|
|
262
|
-
|
|
263
276
|
}
|
|
264
277
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
package com.reactnativecompressor.Audio
|
|
2
|
+
|
|
3
|
+
import android.media.MediaCodec
|
|
4
|
+
import android.media.MediaExtractor
|
|
5
|
+
import android.media.MediaFormat
|
|
6
|
+
import com.reactnativecompressor.Utils.Utils.addLog
|
|
7
|
+
import java.io.BufferedOutputStream
|
|
8
|
+
import java.io.File
|
|
9
|
+
import java.io.FileOutputStream
|
|
10
|
+
import java.io.IOException
|
|
11
|
+
import java.io.RandomAccessFile
|
|
12
|
+
import java.nio.ByteBuffer
|
|
13
|
+
import java.nio.ByteOrder
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Decodes any audio container/codec Android can play (m4a/AAC, mp3, ogg,
|
|
17
|
+
* flac, ...) into a 16-bit PCM little-endian WAV file that LAME can consume.
|
|
18
|
+
*
|
|
19
|
+
* LAME's WaveReader only accepts PCM 16-bit LE WAV, so for any compressed
|
|
20
|
+
* (or non-matching) input we must transcode through a real decoder — jlayer's
|
|
21
|
+
* Converter is MP3-only and silently fails for m4a, ogg, flac, etc.
|
|
22
|
+
*/
|
|
23
|
+
internal object AudioTranscoder {
|
|
24
|
+
|
|
25
|
+
private const val TIMEOUT_US = 10_000L
|
|
26
|
+
private const val WAV_HEADER_SIZE = 44
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decode [srcPath] into a 16-bit PCM WAV at [dstPath].
|
|
30
|
+
*
|
|
31
|
+
* @throws IOException if the file cannot be opened, has no audio track,
|
|
32
|
+
* cannot be decoded on this device, or the output cannot be written.
|
|
33
|
+
*/
|
|
34
|
+
@Throws(IOException::class)
|
|
35
|
+
fun decodeToWav(srcPath: String, dstPath: String) {
|
|
36
|
+
addLog("decodeToWav: $srcPath -> $dstPath")
|
|
37
|
+
|
|
38
|
+
val extractor = MediaExtractor()
|
|
39
|
+
var decoder: MediaCodec? = null
|
|
40
|
+
var output: BufferedOutputStream? = null
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// -- setup extractor -------------------------------------------------
|
|
44
|
+
extractor.setDataSource(srcPath)
|
|
45
|
+
|
|
46
|
+
val audioTrackIndex = findAudioTrackIndex(extractor)
|
|
47
|
+
?: throw IOException("No audio track found in $srcPath")
|
|
48
|
+
extractor.selectTrack(audioTrackIndex)
|
|
49
|
+
|
|
50
|
+
val inputFormat = extractor.getTrackFormat(audioTrackIndex)
|
|
51
|
+
val mime = inputFormat.getString(MediaFormat.KEY_MIME)
|
|
52
|
+
?: throw IOException("Audio track has no mime in $srcPath")
|
|
53
|
+
val sampleRate = if (inputFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
|
|
54
|
+
inputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
|
|
55
|
+
} else {
|
|
56
|
+
throw IOException("Audio track has no sample rate in $srcPath")
|
|
57
|
+
}
|
|
58
|
+
val channelCount = if (inputFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
|
|
59
|
+
inputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
|
|
60
|
+
} else {
|
|
61
|
+
throw IOException("Audio track has no channel count in $srcPath")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// -- create + configure decoder ---------------------------------------
|
|
65
|
+
try {
|
|
66
|
+
decoder = MediaCodec.createDecoderByType(mime)
|
|
67
|
+
} catch (e: IllegalArgumentException) {
|
|
68
|
+
throw IOException("Device cannot decode audio mime $mime", e)
|
|
69
|
+
} catch (e: Exception) {
|
|
70
|
+
throw IOException("Failed to create decoder for $mime", e)
|
|
71
|
+
}
|
|
72
|
+
decoder!!.configure(inputFormat, null, null, 0)
|
|
73
|
+
decoder!!.start()
|
|
74
|
+
|
|
75
|
+
// Capture a non-null val so Kotlin smart-cast works in the loop below.
|
|
76
|
+
val d = decoder!!
|
|
77
|
+
|
|
78
|
+
// -- prepare output WAV file ------------------------------------------
|
|
79
|
+
val outFile = File(dstPath)
|
|
80
|
+
outFile.parentFile?.mkdirs()
|
|
81
|
+
output = BufferedOutputStream(FileOutputStream(outFile))
|
|
82
|
+
// Reserve 44 bytes for the RIFF/WAVE header; we'll come back and
|
|
83
|
+
// overwrite with the correct sizes once we know how many PCM bytes
|
|
84
|
+
// were produced.
|
|
85
|
+
output.write(ByteArray(WAV_HEADER_SIZE))
|
|
86
|
+
|
|
87
|
+
var totalBytesWritten = 0L
|
|
88
|
+
val info = MediaCodec.BufferInfo()
|
|
89
|
+
var inputDone = false
|
|
90
|
+
var outputDone = false
|
|
91
|
+
|
|
92
|
+
// -- decode loop ------------------------------------------------------
|
|
93
|
+
while (!outputDone) {
|
|
94
|
+
// Feed compressed samples into the decoder.
|
|
95
|
+
if (!inputDone) {
|
|
96
|
+
val inIndex = d.dequeueInputBuffer(TIMEOUT_US)
|
|
97
|
+
if (inIndex >= 0) {
|
|
98
|
+
val inBuf = d.getInputBuffer(inIndex)
|
|
99
|
+
?: throw IOException("Decoder returned null input buffer")
|
|
100
|
+
inBuf.clear()
|
|
101
|
+
val sampleSize = extractor.readSampleData(inBuf, 0)
|
|
102
|
+
if (sampleSize < 0) {
|
|
103
|
+
d.queueInputBuffer(
|
|
104
|
+
inIndex, 0, 0, 0,
|
|
105
|
+
MediaCodec.BUFFER_FLAG_END_OF_STREAM
|
|
106
|
+
)
|
|
107
|
+
inputDone = true
|
|
108
|
+
} else {
|
|
109
|
+
d.queueInputBuffer(
|
|
110
|
+
inIndex, 0, sampleSize, extractor.sampleTime, 0
|
|
111
|
+
)
|
|
112
|
+
extractor.advance()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Drain decoded PCM and append it to the file (after the header
|
|
118
|
+
// placeholder).
|
|
119
|
+
val outIndex = d.dequeueOutputBuffer(info, TIMEOUT_US)
|
|
120
|
+
if (outIndex >= 0) {
|
|
121
|
+
val outBuf = d.getOutputBuffer(outIndex)
|
|
122
|
+
?: throw IOException("Decoder returned null output buffer")
|
|
123
|
+
if (info.size > 0) {
|
|
124
|
+
// outBuf is a direct ByteBuffer; data is laid out in native byte
|
|
125
|
+
// order in [info.offset, info.offset + info.size). Copy the whole
|
|
126
|
+
// run in a single contiguous read.
|
|
127
|
+
val copy = ByteArray(info.size)
|
|
128
|
+
outBuf.position(info.offset)
|
|
129
|
+
outBuf.get(copy)
|
|
130
|
+
output.write(copy)
|
|
131
|
+
totalBytesWritten += info.size
|
|
132
|
+
}
|
|
133
|
+
d.releaseOutputBuffer(outIndex, false)
|
|
134
|
+
|
|
135
|
+
if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
|
|
136
|
+
outputDone = true
|
|
137
|
+
}
|
|
138
|
+
} else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
139
|
+
// The final decoded format may differ from what we read at
|
|
140
|
+
// configure time (e.g. AAC decoder may change channel layout
|
|
141
|
+
// after the first frame). Log it for diagnostics; the header we
|
|
142
|
+
// write at the end will use the configure-time values, which is
|
|
143
|
+
// the standard PCM layout that LAME expects.
|
|
144
|
+
addLog("decoder output format changed: ${d.outputFormat}")
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
output.flush()
|
|
149
|
+
|
|
150
|
+
// Patch the WAV header with the final sizes now that we know them.
|
|
151
|
+
writeWavHeader(outFile, sampleRate, channelCount, totalBytesWritten)
|
|
152
|
+
|
|
153
|
+
addLog("decodeToWav: wrote $totalBytesWritten PCM bytes")
|
|
154
|
+
} finally {
|
|
155
|
+
runCatching { decoder?.stop() }
|
|
156
|
+
runCatching { decoder?.release() }
|
|
157
|
+
runCatching { extractor.release() }
|
|
158
|
+
runCatching { output?.close() }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private fun findAudioTrackIndex(extractor: MediaExtractor): Int? {
|
|
163
|
+
for (i in 0 until extractor.trackCount) {
|
|
164
|
+
val mime = extractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME)
|
|
165
|
+
if (mime != null && mime.startsWith("audio/")) return i
|
|
166
|
+
}
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Write a standard 44-byte PCM 16-bit little-endian WAV header into the
|
|
172
|
+
* first [WAV_HEADER_SIZE] bytes of [outFile], anchored to the [dataBytes]
|
|
173
|
+
* sample body that follows.
|
|
174
|
+
*/
|
|
175
|
+
private fun writeWavHeader(outFile: File, sampleRate: Int, channels: Int, dataBytes: Long) {
|
|
176
|
+
val header = ByteBuffer.allocate(WAV_HEADER_SIZE).order(ByteOrder.LITTLE_ENDIAN)
|
|
177
|
+
val byteRate = sampleRate * channels * 2 // 16-bit = 2 bytes per sample
|
|
178
|
+
val blockAlign = (channels * 2).toShort()
|
|
179
|
+
val chunkSize = 36L + dataBytes
|
|
180
|
+
|
|
181
|
+
header.put("RIFF".toByteArray(Charsets.US_ASCII))
|
|
182
|
+
header.putInt(chunkSize.toInt())
|
|
183
|
+
header.put("WAVE".toByteArray(Charsets.US_ASCII))
|
|
184
|
+
header.put("fmt ".toByteArray(Charsets.US_ASCII))
|
|
185
|
+
header.putInt(16) // PCM fmt chunk size
|
|
186
|
+
header.putShort(1) // PCM format
|
|
187
|
+
header.putShort(channels.toShort())
|
|
188
|
+
header.putInt(sampleRate)
|
|
189
|
+
header.putInt(byteRate)
|
|
190
|
+
header.putShort(blockAlign)
|
|
191
|
+
header.putShort(16) // bits per sample
|
|
192
|
+
header.put("data".toByteArray(Charsets.US_ASCII))
|
|
193
|
+
header.putInt(dataBytes.toInt())
|
|
194
|
+
|
|
195
|
+
val raf = RandomAccessFile(outFile, "rw")
|
|
196
|
+
try {
|
|
197
|
+
raf.seek(0)
|
|
198
|
+
raf.write(header.array(), 0, WAV_HEADER_SIZE)
|
|
199
|
+
} finally {
|
|
200
|
+
raf.close()
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
}
|