1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.vdmdemo.common;
18 
19 import static com.google.common.util.concurrent.Uninterruptibles.putUninterruptibly;
20 
21 import android.media.MediaCodec;
22 import android.media.MediaCodec.BufferInfo;
23 import android.media.MediaCodec.CodecException;
24 import android.media.MediaCodecInfo;
25 import android.media.MediaFormat;
26 import android.os.Environment;
27 import android.os.Handler;
28 import android.os.HandlerThread;
29 import android.util.Log;
30 import android.view.Surface;
31 
32 import androidx.annotation.GuardedBy;
33 import androidx.annotation.NonNull;
34 
35 import com.example.android.vdmdemo.common.RemoteEventProto.EncodedFrame;
36 import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
37 import com.google.protobuf.ByteString;
38 
39 import java.io.BufferedOutputStream;
40 import java.io.File;
41 import java.io.FileNotFoundException;
42 import java.io.FileOutputStream;
43 import java.io.IOException;
44 import java.io.OutputStream;
45 import java.nio.ByteBuffer;
46 import java.util.Objects;
47 import java.util.Optional;
48 import java.util.concurrent.BlockingQueue;
49 import java.util.concurrent.LinkedBlockingQueue;
50 import java.util.concurrent.atomic.AtomicBoolean;
51 import java.util.function.Consumer;
52 
53 /** Shared class between the client and the host, managing the video encoding and decoding. */
54 public class VideoManager {
55     private static final String TAG = "VideoManager";
56     private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
57 
58     @GuardedBy("mCodecLock")
59     private MediaCodec mMediaCodec;
60 
61     private final Object mCodecLock = new Object();
62     private final HandlerThread mCallbackThread;
63     private final boolean mRecordEncoderOutput;
64     private final BlockingQueue<EncodedFrame> mEventQueue = new LinkedBlockingQueue<>(100);
65     private final BlockingQueue<Integer> mFreeInputBuffers = new LinkedBlockingQueue<>(100);
66     private final RemoteIo mRemoteIo;
67     private final Consumer<RemoteEvent> mRemoteFrameConsumer = this::processFrameProto;
68     private StorageFile mStorageFile;
69     private DecoderThread mDecoderThread;
70 
71     private VideoManagerProtoHelper mProtoHelper;
72 
73     private interface VideoManagerProtoHelper {
74 
extractEncodedFrame(RemoteEvent event)75         Optional<EncodedFrame> extractEncodedFrame(RemoteEvent event);
76 
createFrameProto(byte[] data, int flags, long presentationTimeUs)77         RemoteEvent createFrameProto(byte[] data, int flags, long presentationTimeUs);
78 
getVideoManagerId()79         String getVideoManagerId();
80     }
81 
VideoManager( VideoManagerProtoHelper protoHelper, RemoteIo remoteIo, MediaCodec mediaCodec, boolean recordEncoderOutput)82     private VideoManager(
83             VideoManagerProtoHelper protoHelper, RemoteIo remoteIo, MediaCodec mediaCodec,
84             boolean recordEncoderOutput) {
85         mProtoHelper = protoHelper;
86         mRemoteIo = remoteIo;
87         mMediaCodec = mediaCodec;
88         mRecordEncoderOutput = recordEncoderOutput;
89 
90         mCallbackThread = new HandlerThread("VideoManager-" + protoHelper.getVideoManagerId());
91         mCallbackThread.start();
92         mediaCodec.setCallback(new MediaCodecCallback(), new Handler(mCallbackThread.getLooper()));
93 
94         if (!mediaCodec.getCodecInfo().isEncoder()) {
95             remoteIo.addMessageConsumer(mRemoteFrameConsumer);
96         }
97 
98         if (recordEncoderOutput) {
99             mStorageFile = new StorageFile(protoHelper.getVideoManagerId());
100         }
101     }
102 
103     /** Creates a VideoManager instance for encoding display stream. */
createDisplayEncoder( int displayId, RemoteIo remoteIo, boolean recordEncoderOutput)104     public static VideoManager createDisplayEncoder(
105             int displayId, RemoteIo remoteIo, boolean recordEncoderOutput) {
106         try {
107             MediaCodec mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
108             return new VideoManager(new DisplayProtoHelper(displayId), remoteIo, mediaCodec,
109                     recordEncoderOutput);
110         } catch (IOException e) {
111             throw new AssertionError("Unhandled exception", e);
112         }
113     }
114 
115     /** Creates a VideoManager instance for decoding display stream. */
createDisplayDecoder(int displayId, RemoteIo remoteIo)116     public static VideoManager createDisplayDecoder(int displayId, RemoteIo remoteIo) {
117         try {
118             MediaCodec mediaCodec = MediaCodec.createDecoderByType(MIME_TYPE);
119             return new VideoManager(new DisplayProtoHelper(displayId), remoteIo, mediaCodec, false);
120         } catch (IOException e) {
121             throw new AssertionError("Unhandled exception", e);
122         }
123     }
124 
125     /** Creates a VideoManager instance for encoding camera stream. */
createCameraEncoder( String cameraId, RemoteIo remoteIo, boolean recordEncoderOutput)126     public static VideoManager createCameraEncoder(
127             String cameraId, RemoteIo remoteIo, boolean recordEncoderOutput) {
128         try {
129             MediaCodec mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
130             return new VideoManager(new CameraProtoHelper(cameraId), remoteIo, mediaCodec,
131                     recordEncoderOutput);
132         } catch (IOException e) {
133             throw new AssertionError("Unhandled exception", e);
134         }
135     }
136 
137     /** Creates a VideoManager instance for decoding camera stream. */
createCameraDecoder(String cameraId, RemoteIo remoteIo)138     public static VideoManager createCameraDecoder(String cameraId, RemoteIo remoteIo) {
139         try {
140             MediaCodec mediaCodec = MediaCodec.createDecoderByType(MIME_TYPE);
141             return new VideoManager(new CameraProtoHelper(cameraId), remoteIo, mediaCodec, false);
142         } catch (IOException e) {
143             throw new AssertionError("Unhandled exception", e);
144         }
145     }
146 
147     /** Stops processing and resets the internal state. */
stop()148     public void stop() {
149         synchronized (mCodecLock) {
150             if (mMediaCodec == null) {
151                 return;
152             }
153             if (mMediaCodec.getCodecInfo().isEncoder()) {
154                 mMediaCodec.signalEndOfInputStream();
155             } else {
156                 mRemoteIo.removeMessageConsumer(mRemoteFrameConsumer);
157                 mEventQueue.clear();
158                 mDecoderThread.exit();
159             }
160             mCallbackThread.quitSafely();
161             try {
162                 mMediaCodec.flush();
163                 mMediaCodec.stop();
164             } catch (IllegalStateException exception) {
165                 // It's possible the codec transitioned from "executing" state, because
166                 // the surface was already released.
167                 Log.w(TAG, "IllegalStateException while flushing codec", exception);
168             }
169             mMediaCodec.release();
170             mMediaCodec = null;
171         }
172         if (mRecordEncoderOutput) {
173             mStorageFile.closeOutputFile();
174         }
175     }
176 
177     /** Creates a surface for encoding. */
createInputSurface(int width, int height, int frameRate)178     public Surface createInputSurface(int width, int height, int frameRate) {
179         MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
180         mediaFormat.setInteger(
181                 MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
182         mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 500000);
183         mediaFormat.setInteger(MediaFormat.KEY_MAX_B_FRAMES, 0);
184         mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
185         mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
186         synchronized (mCodecLock) {
187             mMediaCodec.configure(
188                     mediaFormat, /* surface= */ null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
189             return mMediaCodec.createInputSurface();
190         }
191     }
192 
193     /** Starts encoding. {@link #createInputSurface} must have been called already. */
startEncoding()194     public void startEncoding() {
195         synchronized (mCodecLock) {
196             mMediaCodec.start();
197         }
198     }
199 
200     /** Starts decoding from the given surface. */
startDecoding(Surface surface, int width, int height)201     public void startDecoding(Surface surface, int width, int height) {
202         MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
203         mediaFormat.setInteger(MediaFormat.KEY_LOW_LATENCY, 1);
204         mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 100);
205         synchronized (mCodecLock) {
206             mMediaCodec.configure(mediaFormat, surface, null, 0);
207             mMediaCodec.start();
208         }
209         mDecoderThread = new DecoderThread();
210         mDecoderThread.start();
211     }
212 
processFrameProto(RemoteEvent event)213     private void processFrameProto(RemoteEvent event) {
214         mProtoHelper.extractEncodedFrame(event).ifPresent(
215                 encodedFrame -> putUninterruptibly(mEventQueue, encodedFrame));
216     }
217 
218     private final class MediaCodecCallback extends MediaCodec.Callback {
219         @Override
onInputBufferAvailable(@onNull MediaCodec codec, int i)220         public void onInputBufferAvailable(@NonNull MediaCodec codec, int i) {
221             mFreeInputBuffers.add(i);
222         }
223 
224         @Override
onOutputBufferAvailable( @onNull MediaCodec codec, int i, @NonNull BufferInfo bufferInfo)225         public void onOutputBufferAvailable(
226                 @NonNull MediaCodec codec, int i, @NonNull BufferInfo bufferInfo) {
227             synchronized (mCodecLock) {
228                 if (mMediaCodec == null) {
229                     return;
230                 }
231                 if (mMediaCodec.getCodecInfo().isEncoder()) {
232                     ByteBuffer buffer = mMediaCodec.getOutputBuffer(i);
233                     byte[] data = new byte[bufferInfo.size];
234                     Objects.requireNonNull(buffer).get(data, bufferInfo.offset, bufferInfo.size);
235                     mMediaCodec.releaseOutputBuffer(i, false);
236                     if (mRecordEncoderOutput) {
237                         mStorageFile.writeOutputFile(data);
238                     }
239 
240                     mRemoteIo.sendMessage(
241                             mProtoHelper.createFrameProto(
242                                     data, bufferInfo.flags, bufferInfo.presentationTimeUs));
243                 } else {
244                     try {
245                         mMediaCodec.releaseOutputBuffer(i, true);
246                     } catch (CodecException exception) {
247                         Log.e(TAG, "Codec exception ", exception);
248                     }
249                 }
250             }
251         }
252 
253         @Override
onError(@onNull MediaCodec mediaCodec, @NonNull CodecException e)254         public void onError(@NonNull MediaCodec mediaCodec, @NonNull CodecException e) {
255         }
256 
257         @Override
onOutputFormatChanged( @onNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat)258         public void onOutputFormatChanged(
259                 @NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) {
260         }
261     }
262 
263     private class DecoderThread extends Thread {
264 
265         private final AtomicBoolean mExit = new AtomicBoolean(false);
266 
267         @SuppressWarnings("Interruption")
exit()268         void exit() {
269             mExit.set(true);
270             interrupt();
271         }
272 
273         @Override
run()274         public void run() {
275             while (!(Thread.interrupted() && mExit.get())) {
276                 try {
277                     EncodedFrame encodedFrame = mEventQueue.take();
278                     int inputBuffer = mFreeInputBuffers.take();
279 
280                     synchronized (mCodecLock) {
281                         if (mMediaCodec == null) {
282                             continue;
283                         }
284                         try {
285                             ByteBuffer inBuffer = mMediaCodec.getInputBuffer(inputBuffer);
286                             byte[] data = encodedFrame.getFrameData().toByteArray();
287                             Objects.requireNonNull(inBuffer).put(data);
288                             if (mRecordEncoderOutput) {
289                                 mStorageFile.writeOutputFile(data);
290                             }
291                             mMediaCodec.queueInputBuffer(
292                                     inputBuffer,
293                                     0,
294                                     encodedFrame.getFrameData().size(),
295                                     encodedFrame.getPresentationTimeUs(),
296                                     encodedFrame.getFlags());
297                         } catch (MediaCodec.CodecException exception) {
298                             Log.e(TAG, "MediaCodec exception while queuing input", exception);
299                             mMediaCodec.release();
300                             mMediaCodec = null;
301                         }
302                     }
303                 } catch (InterruptedException e) {
304                     if (mExit.get()) {
305                         break;
306                     }
307                 }
308             }
309         }
310     }
311 
312     private static class StorageFile {
313         private static final String DIR = "Download";
314         private static final String FILENAME = "vdmdemo_encoder_output";
315 
316         private OutputStream mOutputStream;
317 
StorageFile(String id)318         private StorageFile(String id) {
319             String filePath = DIR + "/" + FILENAME + "_" + id + ".h264";
320             File f = new File(Environment.getExternalStorageDirectory(), filePath);
321             try {
322                 mOutputStream = new BufferedOutputStream(new FileOutputStream(f));
323             } catch (FileNotFoundException e) {
324                 Log.e(TAG, "Error creating or opening storage file", e);
325             }
326         }
327 
writeOutputFile(byte[] data)328         private void writeOutputFile(byte[] data) {
329             if (mOutputStream == null) {
330                 return;
331             }
332             try {
333                 mOutputStream.write(data);
334             } catch (IOException e) {
335                 Log.e(TAG, "Error writing to output file", e);
336             }
337         }
338 
closeOutputFile()339         private void closeOutputFile() {
340             if (mOutputStream == null) {
341                 return;
342             }
343             try {
344                 mOutputStream.flush();
345                 mOutputStream.close();
346             } catch (IOException e) {
347                 Log.e(TAG, "Error closing output file", e);
348             }
349         }
350     }
351 
352     private static class DisplayProtoHelper implements VideoManagerProtoHelper {
353         private final int mDisplayId;
354         private int mFrameIndex = 0;
355 
DisplayProtoHelper(int displayId)356         DisplayProtoHelper(int displayId) {
357             mDisplayId = displayId;
358         }
359 
360         @Override
extractEncodedFrame(RemoteEvent event)361         public Optional<EncodedFrame> extractEncodedFrame(RemoteEvent event) {
362             if (event.hasDisplayFrame() && event.getDisplayId() == mDisplayId) {
363                 return Optional.of(event.getDisplayFrame());
364             }
365             return Optional.empty();
366         }
367 
368         @Override
createFrameProto(byte[] data, int flags, long presentationTimeUs)369         public RemoteEvent createFrameProto(byte[] data, int flags, long presentationTimeUs) {
370             return RemoteEvent.newBuilder()
371                     .setDisplayId(mDisplayId)
372                     .setDisplayFrame(
373                             EncodedFrame.newBuilder()
374                                     .setFrameData(ByteString.copyFrom(data))
375                                     .setFrameIndex(mFrameIndex++)
376                                     .setPresentationTimeUs(presentationTimeUs)
377                                     .setFlags(flags))
378                     .build();
379         }
380 
381         @Override
getVideoManagerId()382         public String getVideoManagerId() {
383             return "display" + mDisplayId;
384         }
385     }
386 
387     private static class CameraProtoHelper implements VideoManagerProtoHelper {
388         private final String mCameraId;
389         private int mFrameIndex = 0;
390 
CameraProtoHelper(String cameraId)391         CameraProtoHelper(String cameraId) {
392             mCameraId = cameraId;
393         }
394 
395         @Override
extractEncodedFrame(RemoteEvent event)396         public Optional<EncodedFrame> extractEncodedFrame(RemoteEvent event) {
397             if (event.hasCameraFrame() && event.getCameraFrame().getCameraId().equals(mCameraId)) {
398                 Log.d(TAG, "Received encoded frame "
399                         + event.getCameraFrame().getCameraFrame().getFrameIndex());
400                 return Optional.of(event.getCameraFrame().getCameraFrame());
401             }
402             return Optional.empty();
403         }
404 
405         @Override
createFrameProto(byte[] data, int flags, long presentationTimeUs)406         public RemoteEvent createFrameProto(byte[] data, int flags, long presentationTimeUs) {
407             Log.d(TAG, "Sending " + data.length + "B encoded camera frame");
408             return RemoteEvent.newBuilder()
409                     .setCameraFrame(
410                             RemoteEventProto.CameraFrame.newBuilder()
411                                     .setCameraId(mCameraId)
412                                     .setCameraFrame(
413                                             EncodedFrame.newBuilder()
414                                                     .setFrameData(ByteString.copyFrom(data))
415                                                     .setFrameIndex(mFrameIndex++)
416                                                     .setPresentationTimeUs(presentationTimeUs)
417                                                     .setFlags(flags))
418                     )
419                     .build();
420         }
421 
422         @Override
getVideoManagerId()423         public String getVideoManagerId() {
424             return "camera" + mCameraId;
425         }
426     }
427 }
428