Bij Proof of Concept 4 was gekeken naar een mogelijkheid om VLC in te zetten om een Android device te mirroren. Een project waar veel naar verwezen wordt is spydroid (die gebruik maakt van libstreaming). Spydroid is geschreven om puur en alleen het beeld van de camera te streamen. Daarom wordt op internet veel gevraagd naar de code om ook het beeld te mirror-en. Zelf ben ik hier de challenge aangegaan om dit project te herschrijven en het beeld te streamen in plaats van de camera. Hierbij heb ik het project geanalyseerd en gedebugged om te achterhalen hoe het in elkaar steekt. Dat wordt hieronder verder toegelicht met het voorbeeld van de trein dat gegeven is in het onderdeel Video streaming.
Om het geheugen even op te frissen: bij streaming zijn drie onderdelen van belang:
- het bestand format of in andere woorden de encoded bits. (h.264, MPEG-4, VP6, etc)
- het container format die de encoded bits bij elkaar houdt. (mp4, MPEG-4, flv, etc)
- het streaming protocol waarover het bestand verstuurd wordt.
Omdat we het scherm mirror-en (en niet een video vanuit het geheugen) hebben we meer invloed op al de drie bovenstaande punten. Zo wordt er in de code bepaald welke encoded bits er zijn en welke container er gebruikt moet worden omdat deze fixed zijn. In het geval van video streaming zou deze container en daarmee de encoded bits verschillen, dit zou vervolgens afgevangen moeten worden.
De werking
In het voorbeeld met de trein wordt er eerst een bestelling gedaan. Deze bestelling is de VLC call die gemaakt wordt vanuit de terminal.
vlc --fullscreen rtsp://xxx.xxx.xxx.xxx:8086?screencast=50000-30-1080-2072
vlc –fullscreen geeft aan dat vlc de stream fullscreen moet openen. rtsp://xxx.xxx.xxx.xxx:8086 is de link waarop de stream te bereiken is, de xxx.xxx.xxx.xxx komt overeen met het IP-adres van het Android device. rtsp geeft de soort stream aan (het bovengenoemde punt 3), dit kan rtsp of http zijn (rtsp maakt gebruik van udp http van tcp). 8086 Is de poort waarover de overdracht plaatsvindt, 8086 is voor de rtsp stream in het geval van http zou dit 8080 zijn. Alles achter het ? symbool zijn parameters. Screencast is een door mij toegevoegde key. Deze key is toegevoegd met de onderstaande code.
// UriParser.java else if (param.getName().equalsIgnoreCase("screencast")) { VideoQuality quality = VideoQuality.parseQuality(param.getValue()); // quality must be equal device resolution builder.setVideoQuality(quality).setVideoEncoder(CASTING); }
//SessionBuilder.java public final static int CASTING = 6; case CASTING: CastStream stream2 = new CastStream(); if (mContext != null) stream2.setPreferences(PreferenceManager.getDefaultSharedPreferences(mContext)); session.addVideoTrack(stream2); break; }
Deze key geeft aan welke streaming methode toegepast moet worden. In de originele code waren hier twee varianten voor videostreaming, H264 en H263. Beiden maken hardcoded gebruik van de camera. De values die volgen (50000-30-1080-2072) zijn waarden die gebruikt worden voor de settings van de stream. 5000 is de bitrate, 30 de framerate, 1080 de width van het Android scherm, 2072 de height van het Android scherm.
Nu de bestelling gedaan is moet deze worden teruggestuurd. Dit gebeurt door de puzzelstukjes te verkrijgen en over de wagons te verspreiden. Deze puzzelstukjes zijn de bits van het frame/scherm op een specifiek tijdstip. Met behulp van een MediaProjection in combinatie met MediaCodec verkrijgen we de bits. De MediaProjection verkrijgt de inhoud van het scherm op het specifieke tijdstip. Deze inhoud wordt vervolgens doorgegeven aan een Surface die gecreerd is door de MediaCodec. De MediaCodec levert vervolgens een bitstream (de encoded bits).
De Surface is een weak reference naar de consument waarmee deze geassocieerd is. Dit zorgt ervoor dat de data verkregen door de virtualdisplay (gemaakt met de MediaProjection) te gebruiken is door de MediaCodec.
// the main activity public void onActivityResult(int requestCode, int resultCode, Intent intent) { if (REQUEST_CODE_CAPTURE_PERM == requestCode) { // if the user accepts the screencapture if (resultCode == RESULT_OK) { startScreenCastButton.setText("STOP CAPTURE"); isStarted = true; mustExecuteDrain = true; mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, intent); DisplayManager dm = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE); Display defaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY); if (defaultDisplay == null) { throw new RuntimeException("No display found."); } prepareVideoEncoder(); // Start the video input. mVirtualDisplay = mMediaProjection.createVirtualDisplay("Recording Display", screenWidth, screenHeight, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR /* flags */, mInputSurface, null /* callback */, null /* handler */); // Start the encoders drainEncoder(); } else { // user did not grant permissions } } }
//CastStream.java /** * Video encoding is done by a MediaCodec. */ @SuppressLint("NewApi") @Override protected void encodeWithMediaCodecMethod1() throws RuntimeException, IOException { Log.d(TAG, "Overriding the method Video encoded using the MediaCodec API with a buffer"); EncoderDebugger debugger = EncoderDebugger.debug(mSettings, mQuality.resX, mQuality.resY); final NV21Convertor convertor = debugger.getNV21Convertor(); mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName()); MediaFormat format = MediaFormat.createVideoFormat("video/avc", mQuality.resX, mQuality.resY); // Set some required properties. The media codec may fail if these aren't defined. format.setInteger(MediaFormat.KEY_BIT_RATE, mQuality.bitrate); // 6Mbps format.setInteger(MediaFormat.KEY_FRAME_RATE, mQuality.framerate); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, debugger.getEncoderColorFormat()); format.setInteger(MediaFormat.KEY_CAPTURE_RATE, mQuality.framerate); format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 1 seconds between I-frames mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // Create a MediaCodec encoder and configure it. Get a Surface we can use for recording into. Surface surface = mMediaCodec.createInputSurface(); // this surface will be used by the virtualdisplay and will contain the data of the screen ScreenCapture.setInputSurface(surface); try { mMediaProjectionManager = (MediaProjectionManager) ScreenCapture.getInstance().getSystemService(Context.MEDIA_PROJECTION_SERVICE); ScreenCapture.getInstance().startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(), REQUEST_CODE_CAPTURE_PERM); } catch (Exception e) { Log.e(TAG, e.getLocalizedMessage()); } try { mMediaCodec.start(); } catch (Exception e) { } // The packetizer encapsulates the bit stream in an RTP stream and send it over the network mPacketizer.setInputStream(new MediaCodecInputStream(mMediaCodec)); mPacketizer.start(); mStreaming = true; }
In de bovenstaande code, regel 173, zien we ook de container format (het bovengenoemde punt 2). Deze format avc geeft aan dat de data die we terug verkrijgen (de encoded bits, het bovengenoemde punt 1) in het format van H264 zullen zijn.
De verschillende puzzelstukjes of te wel de bits worden verspreid over verschillende wagons. Deze verschillende wagons staan gelijk aan verschillende packets die verstuurd worden. Deze worden verstuurd met behulp van de H264Packetizer die aanwezig is in het libstreaming project. Deze creëert tevens de rtsp stream die gelijk staat aan het treinspoor uit het voorbeeld.
Bibliografie
Inc, M. (2018). Understanding Video Streaming. Opgehaald van Youtube: https://www.youtube.com/watch?time_continue=1&v=AeJzoqtuf-o
Simon. (2018). libstreaming. Opgehaald van Github: https://github.com/fyhertz/libstreaming
Simon. (2018). spydroid-ipcamera. Opgehaald van Github: https://github.com/fyhertz/spydroid-ipcamera