mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-04-12 19:06:11 +00:00
Remove code shared with limelight-common and implement Android bindings.
This commit is contained in:
36
src/com/limelight/binding/PlatformBinding.java
Normal file
36
src/com/limelight/binding/PlatformBinding.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.limelight.binding;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import com.limelight.binding.audio.AndroidAudioRenderer;
|
||||
import com.limelight.binding.video.AndroidCpuDecoderRenderer;
|
||||
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
|
||||
public class PlatformBinding {
|
||||
public static VideoDecoderRenderer chooseDecoderRenderer() {
|
||||
if (Build.HARDWARE.equals("goldfish")) {
|
||||
// Emulator - don't render video (it's slow!)
|
||||
return null;
|
||||
}
|
||||
/*else if (MediaCodecDecoderRenderer.findSafeDecoder() != null) {
|
||||
// Hardware decoding
|
||||
return new MediaCodecDecoderRenderer();
|
||||
}*/
|
||||
else {
|
||||
// Software decoding
|
||||
return new AndroidCpuDecoderRenderer();
|
||||
}
|
||||
}
|
||||
|
||||
public static String getDeviceName() {
|
||||
String deviceName = android.os.Build.MODEL;
|
||||
deviceName = deviceName.replace(" ", "");
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
public static AudioRenderer getAudioRenderer() {
|
||||
return new AndroidAudioRenderer();
|
||||
}
|
||||
}
|
||||
50
src/com/limelight/binding/audio/AndroidAudioRenderer.java
Normal file
50
src/com/limelight/binding/audio/AndroidAudioRenderer.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.limelight.binding.audio;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
|
||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
|
||||
public class AndroidAudioRenderer implements AudioRenderer {
|
||||
|
||||
private AudioTrack track;
|
||||
|
||||
@Override
|
||||
public void streamInitialized(int channelCount, int sampleRate) {
|
||||
int channelConfig;
|
||||
|
||||
switch (channelCount)
|
||||
{
|
||||
case 1:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||
break;
|
||||
case 2:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Decoder returned unhandled channel count");
|
||||
}
|
||||
|
||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
1024, // 1KB buffer
|
||||
AudioTrack.MODE_STREAM);
|
||||
|
||||
track.play();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playDecodedAudio(short[] audioData, int offset, int length) {
|
||||
track.write(audioData, offset, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void streamClosing() {
|
||||
if (track != null) {
|
||||
track.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
210
src/com/limelight/binding/video/AndroidCpuDecoderRenderer.java
Normal file
210
src/com/limelight/binding/video/AndroidCpuDecoderRenderer.java
Normal file
@@ -0,0 +1,210 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
||||
|
||||
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
private Thread rendererThread;
|
||||
private int targetFps;
|
||||
|
||||
private static final int DECODER_BUFFER_SIZE = 92*1024;
|
||||
private ByteBuffer decoderBuffer;
|
||||
|
||||
// Only sleep if the difference is above this value
|
||||
private static final int WAIT_CEILING_MS = 8;
|
||||
|
||||
private static final int LOW_PERF = 1;
|
||||
private static final int MED_PERF = 2;
|
||||
private static final int HIGH_PERF = 3;
|
||||
|
||||
private int cpuCount = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
private int findOptimalPerformanceLevel() {
|
||||
StringBuilder cpuInfo = new StringBuilder();
|
||||
BufferedReader br = null;
|
||||
try {
|
||||
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
|
||||
for (;;) {
|
||||
int ch = br.read();
|
||||
if (ch == -1)
|
||||
break;
|
||||
cpuInfo.append((char)ch);
|
||||
}
|
||||
|
||||
// Here we're doing very simple heuristics based on CPU model
|
||||
String cpuInfoStr = cpuInfo.toString();
|
||||
|
||||
// We order them from greatest to least for proper detection
|
||||
// of devices with multiple sets of cores (like Exynos 5 Octa)
|
||||
// TODO Make this better
|
||||
if (cpuInfoStr.contains("0xc0f")) {
|
||||
// Cortex-A15
|
||||
return MED_PERF;
|
||||
}
|
||||
else if (cpuInfoStr.contains("0xc09")) {
|
||||
// Cortex-A9
|
||||
return LOW_PERF;
|
||||
}
|
||||
else if (cpuInfoStr.contains("0xc07")) {
|
||||
// Cortex-A7
|
||||
return LOW_PERF;
|
||||
}
|
||||
else {
|
||||
// Didn't have anything we're looking for
|
||||
return MED_PERF;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
} finally {
|
||||
if (br != null) {
|
||||
try {
|
||||
br.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't read cpuinfo, so assume medium
|
||||
return MED_PERF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setup(int width, int height, Object renderTarget, int drFlags) {
|
||||
this.targetFps = 30;
|
||||
|
||||
int perfLevel = findOptimalPerformanceLevel();
|
||||
int threadCount;
|
||||
|
||||
int avcFlags = 0;
|
||||
switch (perfLevel) {
|
||||
case HIGH_PERF:
|
||||
// Single threaded low latency decode is ideal but hard to acheive
|
||||
avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
|
||||
threadCount = 1;
|
||||
break;
|
||||
|
||||
case LOW_PERF:
|
||||
// Disable the loop filter for performance reasons
|
||||
avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
|
||||
AvcDecoder.FAST_BILINEAR_FILTERING |
|
||||
AvcDecoder.FAST_DECODE;
|
||||
|
||||
// Use plenty of threads to try to utilize the CPU as best we can
|
||||
threadCount = cpuCount - 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
case MED_PERF:
|
||||
avcFlags = AvcDecoder.BILINEAR_FILTERING |
|
||||
AvcDecoder.FAST_DECODE;
|
||||
|
||||
// Only use 2 threads to minimize frame processing latency
|
||||
threadCount = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
// If the user wants quality, we'll remove the low IQ flags
|
||||
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
|
||||
// Make sure the loop filter is enabled
|
||||
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
|
||||
|
||||
// Disable the non-compliant speed optimizations
|
||||
avcFlags &= ~AvcDecoder.FAST_DECODE;
|
||||
|
||||
System.out.println("Using high quality decoding");
|
||||
}
|
||||
|
||||
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
|
||||
if (err != 0) {
|
||||
throw new IllegalStateException("AVC decoder initialization failure: "+err);
|
||||
}
|
||||
|
||||
AvcDecoder.setRenderTarget(((SurfaceHolder)renderTarget).getSurface());
|
||||
|
||||
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
||||
|
||||
System.out.println("Using software decoding (performance level: "+perfLevel+")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
rendererThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
long nextFrameTime = System.currentTimeMillis();
|
||||
|
||||
while (!isInterrupted())
|
||||
{
|
||||
long diff = nextFrameTime - System.currentTimeMillis();
|
||||
|
||||
if (diff > WAIT_CEILING_MS) {
|
||||
try {
|
||||
Thread.sleep(diff);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||
AvcDecoder.redraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
rendererThread.setName("Video - Renderer (CPU)");
|
||||
rendererThread.start();
|
||||
}
|
||||
|
||||
private long computePresentationTimeMs(int frameRate) {
|
||||
return System.currentTimeMillis() + (1000 / frameRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
rendererThread.interrupt();
|
||||
|
||||
try {
|
||||
rendererThread.join();
|
||||
} catch (InterruptedException e) { }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
AvcDecoder.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
byte[] data;
|
||||
|
||||
// Use the reserved decoder buffer if this decode unit will fit
|
||||
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
||||
decoderBuffer.clear();
|
||||
|
||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
||||
}
|
||||
|
||||
data = decoderBuffer.array();
|
||||
}
|
||||
else {
|
||||
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
||||
|
||||
int offset = 0;
|
||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
||||
offset += bbd.length;
|
||||
}
|
||||
}
|
||||
|
||||
return (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
|
||||
}
|
||||
}
|
||||
184
src/com/limelight/binding/video/MediaCodecDecoderRenderer.java
Normal file
184
src/com/limelight/binding/video/MediaCodecDecoderRenderer.java
Normal file
@@ -0,0 +1,184 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecList;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaCodec.BufferInfo;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
private ByteBuffer[] videoDecoderInputBuffers;
|
||||
private MediaCodec videoDecoder;
|
||||
private Thread rendererThread;
|
||||
|
||||
public static final List<String> blacklistedDecoderPrefixes;
|
||||
|
||||
static {
|
||||
blacklistedDecoderPrefixes = new LinkedList<String>();
|
||||
blacklistedDecoderPrefixes.add("omx.google");
|
||||
blacklistedDecoderPrefixes.add("omx.nvidia");
|
||||
blacklistedDecoderPrefixes.add("omx.TI");
|
||||
blacklistedDecoderPrefixes.add("omx.RK");
|
||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||
}
|
||||
|
||||
public static MediaCodecInfo findSafeDecoder() {
|
||||
|
||||
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
|
||||
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
|
||||
boolean badCodec = false;
|
||||
|
||||
// Skip encoders
|
||||
if (codecInfo.isEncoder()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String badPrefix : blacklistedDecoderPrefixes) {
|
||||
String name = codecInfo.getName();
|
||||
if (name.length() >= badPrefix.length()) {
|
||||
String prefix = name.substring(0, badPrefix.length());
|
||||
if (prefix.equalsIgnoreCase(badPrefix)) {
|
||||
badCodec = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (badCodec) {
|
||||
System.out.println("Blacklisted decoder: "+codecInfo.getName());
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String mime : codecInfo.getSupportedTypes()) {
|
||||
if (mime.equalsIgnoreCase("video/avc")) {
|
||||
System.out.println("Selected decoder: "+codecInfo.getName());
|
||||
return codecInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setup(int width, int height, Object renderTarget, int drFlags) {
|
||||
videoDecoder = MediaCodec.createByCodecName(findSafeDecoder().getName());
|
||||
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
|
||||
|
||||
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
||||
|
||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||
|
||||
videoDecoder.start();
|
||||
|
||||
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
||||
|
||||
System.out.println("Using hardware decoding");
|
||||
}
|
||||
|
||||
private void startRendererThread()
|
||||
{
|
||||
rendererThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
long nextFrameTimeUs = 0;
|
||||
while (!isInterrupted())
|
||||
{
|
||||
BufferInfo info = new BufferInfo();
|
||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, 100);
|
||||
switch (outIndex) {
|
||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
||||
System.out.println("Output buffers changed");
|
||||
break;
|
||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||
System.out.println("Output format changed");
|
||||
System.out.println("New output Format: " + videoDecoder.getOutputFormat());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (outIndex >= 0) {
|
||||
boolean render = false;
|
||||
|
||||
if (currentTimeUs() >= nextFrameTimeUs) {
|
||||
render = true;
|
||||
nextFrameTimeUs = computePresentationTime(60);
|
||||
}
|
||||
|
||||
videoDecoder.releaseOutputBuffer(outIndex, render);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
rendererThread.setName("Video - Renderer (MediaCodec)");
|
||||
rendererThread.start();
|
||||
}
|
||||
|
||||
private static long currentTimeUs() {
|
||||
return System.nanoTime() / 1000;
|
||||
}
|
||||
|
||||
private long computePresentationTime(int frameRate) {
|
||||
return currentTimeUs() + (1000000 / frameRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
startRendererThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
rendererThread.interrupt();
|
||||
|
||||
try {
|
||||
rendererThread.join();
|
||||
} catch (InterruptedException e) { }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (videoDecoder != null) {
|
||||
videoDecoder.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
if (decodeUnit.getType() != DecodeUnit.TYPE_H264) {
|
||||
System.err.println("Unknown decode unit type");
|
||||
return false;
|
||||
}
|
||||
|
||||
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
|
||||
if (inputIndex >= 0)
|
||||
{
|
||||
ByteBuffer buf = videoDecoderInputBuffers[inputIndex];
|
||||
|
||||
// Clear old input data
|
||||
buf.clear();
|
||||
|
||||
// Copy data from our buffer list into the input buffer
|
||||
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
|
||||
{
|
||||
buf.put(desc.data, desc.offset, desc.length);
|
||||
}
|
||||
|
||||
videoDecoder.queueInputBuffer(inputIndex,
|
||||
0, decodeUnit.getDataLength(),
|
||||
0, decodeUnit.getFlags());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
36
src/com/limelight/binding/video/RsRenderer.java
Normal file
36
src/com/limelight/binding/video/RsRenderer.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import android.content.Context;
|
||||
import android.renderscript.Allocation;
|
||||
import android.renderscript.Element;
|
||||
import android.renderscript.RenderScript;
|
||||
import android.renderscript.Type;
|
||||
import android.view.Surface;
|
||||
|
||||
public class RsRenderer {
|
||||
private RenderScript rs;
|
||||
private Allocation renderBuffer;
|
||||
|
||||
public RsRenderer(Context context, int width, int height, Surface renderTarget) {
|
||||
rs = RenderScript.create(context);
|
||||
|
||||
Type.Builder tb = new Type.Builder(rs, Element.RGBA_8888(rs));
|
||||
tb.setX(width);
|
||||
tb.setY(height);
|
||||
Type bufferType = tb.create();
|
||||
|
||||
renderBuffer = Allocation.createTyped(rs, bufferType, Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT);
|
||||
renderBuffer.setSurface(renderTarget);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
renderBuffer.setSurface(null);
|
||||
renderBuffer.destroy();
|
||||
rs.destroy();
|
||||
}
|
||||
|
||||
public void render(byte[] rgbData) {
|
||||
renderBuffer.copyFrom(rgbData);
|
||||
renderBuffer.ioSend();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user