VideoDecoder.java 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. package videodecode;
  19. import android.content.res.AssetFileDescriptor;
  20. import android.media.MediaCodec;
  21. import android.media.MediaCodec.BufferInfo;
  22. import android.media.MediaExtractor;
  23. import android.media.MediaFormat;
  24. import android.content.Context;
  25. import android.util.Log;
  26. import java.nio.ByteBuffer;
  27. /**
  28. * Hardware-accelerated video decoder using Android MediaCodec.
  29. *
  30. * Decodes video files (H.264/AVC, etc.) and extracts YUV frames for processing.
  31. * Handles different YUV color formats (planar and semi-planar) across devices.
  32. *
  33. * Usage:
  34. * <pre>
  35. * VideoDecoder decoder = new VideoDecoder();
  36. * decoder.setContext(context);
  37. * decoder.init("video.mp4");
  38. * int frameCount = decoder.decodeAll();
  39. * </pre>
  40. */
  41. public class VideoDecoder {
  42. private static final boolean DEBUG = false;
  43. /** YUV420 planar format (YV12/I420) - separate Y, U, V planes */
  44. private static final int COLOR_FORMATYUV420_PLANAR = 19;
  45. /** YUV420 semi-planar format (NV21) - Y plane + interleaved UV plane */
  46. private static final int COLOR_FORMATYUV420_SEMIPLANAR = 21;
  47. private MediaCodec decoder;
  48. private MediaExtractor extractor;
  49. private int width;
  50. private int height;
  51. private int decoderId;
  52. private int outputColorFormat = -1;
  53. private Context context;
  54. private int stride = -1;
  55. private int sliceHeight = -1;
  56. /** Conditional logging based on DEBUG flag */
  57. private void log(String fstr, Object... args) {
  58. if (!DEBUG) return;
  59. Log.d("darkfi", String.format(fstr, args));
  60. }
  61. /** Native callback invoked when a frame is decoded and YUV data is extracted */
  62. native void onFrameDecoded(int decoderId, byte[] yData, byte[] uData, byte[] vData, int width, int height);
  63. /** Sets the decoder ID for native callbacks */
  64. public void setDecoderId(int id) {
  65. this.decoderId = id;
  66. }
  67. /** Sets the Android context for asset loading */
  68. public void setContext(Context ctx) {
  69. this.context = ctx;
  70. }
  71. /**
  72. * Initializes the video decoder for a given asset path.
  73. *
  74. * Attempts to load from app assets first, falls back to file path.
  75. * Finds the video track, extracts dimensions, and configures MediaCodec.
  76. *
  77. * @param assetPath Path to video file (asset name or absolute path)
  78. * @return true if initialization succeeded, false otherwise
  79. */
  80. public boolean init(String assetPath) {
  81. try {
  82. log("init(%s)", assetPath);
  83. extractor = new MediaExtractor();
  84. try {
  85. AssetFileDescriptor afd = context.getAssets().openFd(assetPath);
  86. extractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getDeclaredLength());
  87. } catch (Exception e) {
  88. extractor.setDataSource(assetPath);
  89. }
  90. MediaFormat format = null;
  91. for (int i = 0; i < extractor.getTrackCount(); i++) {
  92. format = extractor.getTrackFormat(i);
  93. String mime = format.getString(MediaFormat.KEY_MIME);
  94. if (mime != null && mime.startsWith("video/")) {
  95. extractor.selectTrack(i);
  96. break;
  97. }
  98. format = null;
  99. }
  100. if (format == null) {
  101. Log.e("darkfi", "No video track found in: " + assetPath);
  102. return false;
  103. }
  104. width = format.getInteger(MediaFormat.KEY_WIDTH);
  105. height = format.getInteger(MediaFormat.KEY_HEIGHT);
  106. decoder = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME));
  107. decoder.configure(format, null, null, 0);
  108. log("Initialized decoder: %dx%d mime=%s", width, height, format.getString(MediaFormat.KEY_MIME));
  109. return true;
  110. } catch (Exception e) {
  111. Log.e("darkfi", "Failed to initialize video decoder: " + e.getMessage(), e);
  112. return false;
  113. }
  114. }
  115. /**
  116. * Decodes all frames from the video.
  117. *
  118. * Processes the entire video, extracting YUV data from each frame
  119. * and invoking the native callback. Handles format changes during decoding.
  120. *
  121. * @return Number of frames decoded, or -1 on error
  122. */
  123. public int decodeAll() {
  124. if (decoder == null || extractor == null) {
  125. Log.e("darkfi", "Decoder not initialized");
  126. return -1;
  127. }
  128. decoder.start();
  129. MediaFormat outputFormat = decoder.getOutputFormat();
  130. if (outputFormat != null && outputFormat.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
  131. outputColorFormat = outputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT);
  132. }
  133. log("decodeAll() colorFormat=%d", outputColorFormat);
  134. int frameIndex = 0;
  135. boolean inputEOS = false;
  136. boolean outputEOS = false;
  137. BufferInfo bufferInfo = new BufferInfo();
  138. try {
  139. while (!outputEOS) {
  140. if (!inputEOS) {
  141. inputEOS = processInput();
  142. }
  143. int result = processOutput(bufferInfo);
  144. if (result >= 0) {
  145. frameIndex += result;
  146. }
  147. if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
  148. outputEOS = true;
  149. }
  150. }
  151. } finally {
  152. decoder.stop();
  153. decoder.release();
  154. extractor.release();
  155. }
  156. log("Decoding complete: %d frames", frameIndex);
  157. return frameIndex;
  158. }
  159. /**
  160. * Feeds compressed video data from the extractor to the decoder.
  161. *
  162. * @return true if end of stream was reached, false otherwise
  163. */
  164. private boolean processInput() {
  165. // Try to read some data
  166. int inputBufferId = decoder.dequeueInputBuffer(10000);
  167. // Not yet available
  168. if (inputBufferId < 0)
  169. return false;
  170. ByteBuffer inputBuffer = decoder.getInputBuffer(inputBufferId);
  171. int sampleSize = extractor.readSampleData(inputBuffer, 0);
  172. // Negative sampleSize means extractor reached end of file
  173. if (sampleSize < 0) {
  174. decoder.queueInputBuffer(inputBufferId, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
  175. return true;
  176. }
  177. // Success
  178. decoder.queueInputBuffer(inputBufferId, 0, sampleSize, extractor.getSampleTime(), 0);
  179. extractor.advance();
  180. return false;
  181. }
  182. /**
  183. * Retrieves and processes decoded frames from the decoder.
  184. *
  185. * @param bufferInfo BufferInfo object to populate with frame metadata
  186. * @return Number of frames processed (0 or 1), or negative value for info events
  187. */
  188. private int processOutput(BufferInfo bufferInfo) {
  189. int outputBufferId = decoder.dequeueOutputBuffer(bufferInfo, 10000);
  190. if (outputBufferId >= 0) {
  191. // New frame to read
  192. ByteBuffer outputBuffer = decoder.getOutputBuffer(outputBufferId);
  193. if (outputBuffer != null) {
  194. processOutputBuffer(outputBuffer, bufferInfo.offset, bufferInfo.size);
  195. }
  196. decoder.releaseOutputBuffer(outputBufferId, false);
  197. return 1;
  198. } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
  199. // Ready to read the output format
  200. MediaFormat newFormat = decoder.getOutputFormat();
  201. // We are interested in the color format
  202. if (newFormat.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
  203. outputColorFormat = newFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT);
  204. log("Format changed: colorFormat=%d", outputColorFormat);
  205. }
  206. // Extract stride and slice-height for handling padded buffers
  207. if (newFormat.containsKey(MediaFormat.KEY_STRIDE)) {
  208. stride = newFormat.getInteger(MediaFormat.KEY_STRIDE);
  209. }
  210. if (newFormat.containsKey(MediaFormat.KEY_SLICE_HEIGHT)) {
  211. sliceHeight = newFormat.getInteger(MediaFormat.KEY_SLICE_HEIGHT);
  212. }
  213. log("Format changed: stride=%d, slice-height=%d", stride, sliceHeight);
  214. }
  215. return 0;
  216. }
  217. /**
  218. * Processes a decoded video frame buffer and extracts YUV data.
  219. *
  220. * Handles different color formats:
  221. * - Semi-planar (NV21): De-interleaves UV data
  222. * - Planar (YV12/I420): Reads U and V planes directly
  223. *
  224. * Handles stride padding when hardware decoder uses row alignment.
  225. *
  226. * @param outputBuffer Raw decoded frame data from MediaCodec
  227. * @param offset Offset to valid data in buffer
  228. * @param size Size of valid data in bytes
  229. */
  230. private void processOutputBuffer(ByteBuffer outputBuffer, int offset, int size) {
  231. outputBuffer.position(offset);
  232. outputBuffer.limit(offset + size);
  233. int ySize = width * height;
  234. int uvSize = (width / 2) * (height / 2);
  235. byte[] yData = new byte[ySize];
  236. byte[] uData = new byte[uvSize];
  237. byte[] vData = new byte[uvSize];
  238. // Read Y plane, accounting for stride if present
  239. int yStride = (stride > 0) ? stride : width;
  240. int yRows = (sliceHeight > 0) ? sliceHeight : height;
  241. // Read Y plane row by row, skipping padding
  242. for (int row = 0; row < height && row < yRows; row++) {
  243. int destOffset = row * width;
  244. outputBuffer.get(yData, destOffset, width);
  245. if (row < yRows - 1) {
  246. // Skip padding bytes to next row
  247. int skip = yStride - width;
  248. outputBuffer.position(outputBuffer.position() + skip);
  249. }
  250. }
  251. // Seek to UV plane start: Y plane ends at yStride * yRows
  252. int uvPlaneStart = yStride * yRows;
  253. outputBuffer.position(offset + uvPlaneStart);
  254. if (outputColorFormat == COLOR_FORMATYUV420_PLANAR) {
  255. outputBuffer.get(uData, 0, uvSize);
  256. outputBuffer.get(vData, 0, uvSize);
  257. } else {
  258. if (outputColorFormat != COLOR_FORMATYUV420_SEMIPLANAR) {
  259. Log.w("darkfi", String.format("Unknown color format %d, assuming semi-planar", outputColorFormat));
  260. }
  261. // For semi-planar, UV plane may also have stride
  262. deinterleaveUV(outputBuffer, uData, vData, uvSize, yStride);
  263. }
  264. onFrameDecoded(decoderId, yData, uData, vData, width, height);
  265. outputBuffer.clear();
  266. }
  267. /**
  268. * De-interleaves UV data from semi-planar YUV format (NV21).
  269. *
  270. * Handles stride padding in the UV plane.
  271. *
  272. * @param outputBuffer Buffer containing interleaved UVUV... data
  273. * @param uData Output array for U component
  274. * @param vData Output array for V component
  275. * @param uvSize Number of UV pairs to de-interleave
  276. * @param yStride Luma stride (used to calculate chroma stride)
  277. */
  278. private void deinterleaveUV(ByteBuffer outputBuffer, byte[] uData, byte[] vData, int uvSize, int yStride) {
  279. int uvWidth = width / 2;
  280. int uvHeight = height / 2;
  281. int uvStride = (yStride > 0) ? (yStride + 1) / 2 : uvWidth;
  282. // Read UV plane row by row, deinterleaving and skipping padding
  283. int uvIdx = 0;
  284. for (int row = 0; row < uvHeight; row++) {
  285. // Read one row of UV pairs (interleaved)
  286. for (int col = 0; col < uvWidth; col++) {
  287. int uByte = outputBuffer.get() & 0xFF;
  288. int vByte = outputBuffer.get() & 0xFF;
  289. uData[uvIdx] = (byte) uByte;
  290. vData[uvIdx] = (byte) vByte;
  291. uvIdx++;
  292. }
  293. // Skip padding to next row
  294. if (row < uvHeight - 1) {
  295. int skip = (uvStride - uvWidth) * 2;
  296. outputBuffer.position(outputBuffer.position() + skip);
  297. }
  298. }
  299. }
  300. }