From 7049236590c430246eae16ac8c2b5e64eed24e24 Mon Sep 17 00:00:00 2001 From: prakarnwongsanit Date: Mon, 22 Dec 2014 13:09:20 +0700 Subject: [PATCH 1/3] Config project to can build with gradle :) --- AndroidManifest.xml | 10 - .../streaming/MediaStream.java | 339 ------- .../majorkernelpanic/streaming/Session.java | 745 --------------- .../streaming/SessionBuilder.java | 313 ------- .../majorkernelpanic/streaming/Stream.java | 119 --- .../streaming/audio/AACStream.java | 377 -------- .../streaming/audio/AMRNBStream.java | 89 -- .../streaming/audio/AudioQuality.java | 70 -- .../streaming/audio/AudioStream.java | 104 --- .../exceptions/CameraInUseException.java | 30 - .../exceptions/ConfNotSupportedException.java | 30 - .../exceptions/InvalidSurfaceException.java | 31 - .../StorageUnavailableException.java | 32 - .../streaming/gl/SurfaceManager.java | 196 ---- .../streaming/gl/SurfaceView.java | 331 ------- .../streaming/gl/TextureManager.java | 288 ------ .../streaming/hw/CodecManager.java | 160 ---- .../streaming/hw/EncoderDebugger.java | 845 ------------------ .../streaming/hw/NV21Convertor.java | 166 ---- .../streaming/mp4/MP4Config.java | 97 -- .../streaming/mp4/MP4Parser.java | 255 ------ .../streaming/rtcp/SenderReport.java | 226 ----- .../streaming/rtp/AACADTSPacketizer.java | 188 ---- .../streaming/rtp/AACLATMPacketizer.java | 135 --- .../streaming/rtp/AMRNBPacketizer.java | 138 --- .../streaming/rtp/AbstractPacketizer.java | 165 ---- .../streaming/rtp/H263Packetizer.java | 149 --- .../streaming/rtp/H264Packetizer.java | 277 ------ .../streaming/rtp/MediaCodecInputStream.java | 122 --- .../streaming/rtp/RtpSocket.java | 451 ---------- .../streaming/rtsp/RtcpDeinterleaver.java | 72 -- .../streaming/rtsp/RtspClient.java | 607 ------------- .../streaming/rtsp/RtspServer.java | 655 -------------- .../streaming/rtsp/UriParser.java | 207 ----- .../streaming/video/CodecManager.java | 265 ------ .../streaming/video/H263Stream.java | 86 -- .../streaming/video/H264Stream.java | 280 ------ .../streaming/video/VideoQuality.java | 147 --- .../streaming/video/VideoStream.java | 729 --------------- 39 files changed, 9526 deletions(-) delete mode 100644 AndroidManifest.xml delete mode 100644 src/net/majorkernelpanic/streaming/MediaStream.java delete mode 100644 src/net/majorkernelpanic/streaming/Session.java delete mode 100644 src/net/majorkernelpanic/streaming/SessionBuilder.java delete mode 100644 src/net/majorkernelpanic/streaming/Stream.java delete mode 100644 src/net/majorkernelpanic/streaming/audio/AACStream.java delete mode 100644 src/net/majorkernelpanic/streaming/audio/AMRNBStream.java delete mode 100644 src/net/majorkernelpanic/streaming/audio/AudioQuality.java delete mode 100644 src/net/majorkernelpanic/streaming/audio/AudioStream.java delete mode 100644 src/net/majorkernelpanic/streaming/exceptions/CameraInUseException.java delete mode 100644 src/net/majorkernelpanic/streaming/exceptions/ConfNotSupportedException.java delete mode 100644 src/net/majorkernelpanic/streaming/exceptions/InvalidSurfaceException.java delete mode 100644 src/net/majorkernelpanic/streaming/exceptions/StorageUnavailableException.java delete mode 100644 src/net/majorkernelpanic/streaming/gl/SurfaceManager.java delete mode 100644 src/net/majorkernelpanic/streaming/gl/SurfaceView.java delete mode 100644 src/net/majorkernelpanic/streaming/gl/TextureManager.java delete mode 100644 src/net/majorkernelpanic/streaming/hw/CodecManager.java delete mode 100644 src/net/majorkernelpanic/streaming/hw/EncoderDebugger.java delete mode 100644 src/net/majorkernelpanic/streaming/hw/NV21Convertor.java delete mode 100644 src/net/majorkernelpanic/streaming/mp4/MP4Config.java delete mode 100644 src/net/majorkernelpanic/streaming/mp4/MP4Parser.java delete mode 100644 src/net/majorkernelpanic/streaming/rtcp/SenderReport.java delete mode 100644 src/net/majorkernelpanic/streaming/rtp/AACADTSPacketizer.java delete mode 100644 src/net/majorkernelpanic/streaming/rtp/AACLATMPacketizer.java delete mode 100644 src/net/majorkernelpanic/streaming/rtp/AMRNBPacketizer.java delete mode 100644 src/net/majorkernelpanic/streaming/rtp/AbstractPacketizer.java delete mode 100644 src/net/majorkernelpanic/streaming/rtp/H263Packetizer.java delete mode 100644 src/net/majorkernelpanic/streaming/rtp/H264Packetizer.java delete mode 100644 src/net/majorkernelpanic/streaming/rtp/MediaCodecInputStream.java delete mode 100644 src/net/majorkernelpanic/streaming/rtp/RtpSocket.java delete mode 100644 src/net/majorkernelpanic/streaming/rtsp/RtcpDeinterleaver.java delete mode 100644 src/net/majorkernelpanic/streaming/rtsp/RtspClient.java delete mode 100644 src/net/majorkernelpanic/streaming/rtsp/RtspServer.java delete mode 100644 src/net/majorkernelpanic/streaming/rtsp/UriParser.java delete mode 100644 src/net/majorkernelpanic/streaming/video/CodecManager.java delete mode 100644 src/net/majorkernelpanic/streaming/video/H263Stream.java delete mode 100644 src/net/majorkernelpanic/streaming/video/H264Stream.java delete mode 100644 src/net/majorkernelpanic/streaming/video/VideoQuality.java delete mode 100644 src/net/majorkernelpanic/streaming/video/VideoStream.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml deleted file mode 100644 index 9e2e14dc..00000000 --- a/AndroidManifest.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/src/net/majorkernelpanic/streaming/MediaStream.java b/src/net/majorkernelpanic/streaming/MediaStream.java deleted file mode 100644 index 95fcecdb..00000000 --- a/src/net/majorkernelpanic/streaming/MediaStream.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetAddress; -import java.util.Random; - -import net.majorkernelpanic.streaming.audio.AudioStream; -import net.majorkernelpanic.streaming.rtp.AbstractPacketizer; -import net.majorkernelpanic.streaming.video.VideoStream; -import android.annotation.SuppressLint; -import android.media.MediaCodec; -import android.media.MediaRecorder; -import android.net.LocalServerSocket; -import android.net.LocalSocket; -import android.net.LocalSocketAddress; -import android.util.Log; - -/** - * A MediaRecorder that streams what it records using a packetizer from the RTP package. - * You can't use this class directly ! - */ -public abstract class MediaStream implements Stream { - - protected static final String TAG = "MediaStream"; - - /** Raw audio/video will be encoded using the MediaRecorder API. */ - public static final byte MODE_MEDIARECORDER_API = 0x01; - - /** Raw audio/video will be encoded using the MediaCodec API with buffers. */ - public static final byte MODE_MEDIACODEC_API = 0x02; - - /** Raw audio/video will be encoded using the MediaCode API with a surface. */ - public static final byte MODE_MEDIACODEC_API_2 = 0x05; - - /** Prefix that will be used for all shared preferences saved by libstreaming */ - protected static final String PREF_PREFIX = "libstreaming-"; - - /** The packetizer that will read the output of the camera and send RTP packets over the networked. */ - protected AbstractPacketizer mPacketizer = null; - - protected static byte sSuggestedMode = MODE_MEDIARECORDER_API; - protected byte mMode, mRequestedMode; - - protected boolean mStreaming = false, mConfigured = false; - protected int mRtpPort = 0, mRtcpPort = 0; - protected byte mChannelIdentifier = 0; - protected OutputStream mOutputStream = null; - protected InetAddress mDestination; - protected LocalSocket mReceiver, mSender = null; - private LocalServerSocket mLss = null; - private int mSocketId, mTTL = 64; - - protected MediaRecorder mMediaRecorder; - protected MediaCodec mMediaCodec; - - static { - // We determine whether or not the MediaCodec API should be used - try { - Class.forName("android.media.MediaCodec"); - // Will be set to MODE_MEDIACODEC_API at some point... - sSuggestedMode = MODE_MEDIACODEC_API; - Log.i(TAG,"Phone supports the MediaCoded API"); - } catch (ClassNotFoundException e) { - sSuggestedMode = MODE_MEDIARECORDER_API; - Log.i(TAG,"Phone does not support the MediaCodec API"); - } - } - - public MediaStream() { - mRequestedMode = sSuggestedMode; - mMode = sSuggestedMode; - } - - /** - * Sets the destination ip address of the stream. - * @param dest The destination address of the stream - */ - public void setDestinationAddress(InetAddress dest) { - mDestination = dest; - } - - /** - * Sets the destination ports of the stream. - * If an odd number is supplied for the destination port then the next - * lower even number will be used for RTP and it will be used for RTCP. - * If an even number is supplied, it will be used for RTP and the next odd - * number will be used for RTCP. - * @param dport The destination port - */ - public void setDestinationPorts(int dport) { - if (dport % 2 == 1) { - mRtpPort = dport-1; - mRtcpPort = dport; - } else { - mRtpPort = dport; - mRtcpPort = dport+1; - } - } - - /** - * Sets the destination ports of the stream. - * @param rtpPort Destination port that will be used for RTP - * @param rtcpPort Destination port that will be used for RTCP - */ - public void setDestinationPorts(int rtpPort, int rtcpPort) { - mRtpPort = rtpPort; - mRtcpPort = rtcpPort; - mOutputStream = null; - } - - /** - * If a TCP is used as the transport protocol for the RTP session, - * the output stream to which RTP packets will be written to must - * be specified with this method. - */ - public void setOutputStream(OutputStream stream, byte channelIdentifier) { - mOutputStream = stream; - mChannelIdentifier = channelIdentifier; - } - - - /** - * Sets the Time To Live of packets sent over the network. - * @param ttl The time to live - * @throws IOException - */ - public void setTimeToLive(int ttl) throws IOException { - mTTL = ttl; - } - - /** - * Returns a pair of destination ports, the first one is the - * one used for RTP and the second one is used for RTCP. - **/ - public int[] getDestinationPorts() { - return new int[] { - mRtpPort, - mRtcpPort - }; - } - - /** - * Returns a pair of source ports, the first one is the - * one used for RTP and the second one is used for RTCP. - **/ - public int[] getLocalPorts() { - return mPacketizer.getRtpSocket().getLocalPorts(); - } - - /** - * Sets the streaming method that will be used. - * - * If the mode is set to {@link #MODE_MEDIARECORDER_API}, raw audio/video will be encoded - * using the MediaRecorder API.
- * - * If the mode is set to {@link #MODE_MEDIACODEC_API} or to {@link #MODE_MEDIACODEC_API_2}, - * audio/video will be encoded with using the MediaCodec.
- * - * The {@link #MODE_MEDIACODEC_API_2} mode only concerns {@link VideoStream}, it makes - * use of the createInputSurface() method of the MediaCodec API (Android 4.3 is needed there).
- * - * @param mode Can be {@link #MODE_MEDIARECORDER_API}, {@link #MODE_MEDIACODEC_API} or {@link #MODE_MEDIACODEC_API_2} - */ - public void setStreamingMethod(byte mode) { - mRequestedMode = mode; - } - - /** - * Returns the streaming method in use, call this after - * {@link #configure()} to get an accurate response. - */ - public byte getStreamingMethod() { - return mMode; - } - - /** - * Returns the packetizer associated with the {@link MediaStream}. - * @return The packetizer - */ - public AbstractPacketizer getPacketizer() { - return mPacketizer; - } - - /** - * Returns an approximation of the bit rate consumed by the stream in bit per seconde. - */ - public long getBitrate() { - return !mStreaming ? 0 : mPacketizer.getRtpSocket().getBitrate(); - } - - /** - * Indicates if the {@link MediaStream} is streaming. - * @return A boolean indicating if the {@link MediaStream} is streaming - */ - public boolean isStreaming() { - return mStreaming; - } - - /** - * Configures the stream with the settings supplied with - * {@link VideoStream#setVideoQuality(net.majorkernelpanic.streaming.video.VideoQuality)} - * for a {@link VideoStream} and {@link AudioStream#setAudioQuality(net.majorkernelpanic.streaming.audio.AudioQuality)} - * for a {@link AudioStream}. - */ - public synchronized void configure() throws IllegalStateException, IOException { - if (mStreaming) throw new IllegalStateException("Can't be called while streaming."); - if (mPacketizer != null) { - mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort); - mPacketizer.getRtpSocket().setOutputStream(mOutputStream, mChannelIdentifier); - } - mMode = mRequestedMode; - mConfigured = true; - } - - /** Starts the stream. */ - public synchronized void start() throws IllegalStateException, IOException { - - if (mDestination==null) - throw new IllegalStateException("No destination ip address set for the stream !"); - - if (mRtpPort<=0 || mRtcpPort<=0) - throw new IllegalStateException("No destination ports set for the stream !"); - - mPacketizer.setTimeToLive(mTTL); - - if (mMode != MODE_MEDIARECORDER_API) { - encodeWithMediaCodec(); - } else { - encodeWithMediaRecorder(); - } - - } - - /** Stops the stream. */ - @SuppressLint("NewApi") - public synchronized void stop() { - if (mStreaming) { - try { - if (mMode==MODE_MEDIARECORDER_API) { - mMediaRecorder.stop(); - mMediaRecorder.release(); - mMediaRecorder = null; - closeSockets(); - mPacketizer.stop(); - } else { - mPacketizer.stop(); - mMediaCodec.stop(); - mMediaCodec.release(); - mMediaCodec = null; - } - } catch (Exception e) { - e.printStackTrace(); - } - mStreaming = false; - } - } - - protected abstract void encodeWithMediaRecorder() throws IOException; - - protected abstract void encodeWithMediaCodec() throws IOException; - - /** - * Returns a description of the stream using SDP. - * This method can only be called after {@link Stream#configure()}. - * @throws IllegalStateException Thrown when {@link Stream#configure()} was not called. - */ - public abstract String getSessionDescription(); - - /** - * Returns the SSRC of the underlying {@link net.majorkernelpanic.streaming.rtp.RtpSocket}. - * @return the SSRC of the stream - */ - public int getSSRC() { - return getPacketizer().getSSRC(); - } - - protected void createSockets() throws IOException { - - final String LOCAL_ADDR = "net.majorkernelpanic.streaming-"; - - for (int i=0;i<10;i++) { - try { - mSocketId = new Random().nextInt(); - mLss = new LocalServerSocket(LOCAL_ADDR+mSocketId); - break; - } catch (IOException e1) {} - } - - mReceiver = new LocalSocket(); - mReceiver.connect( new LocalSocketAddress(LOCAL_ADDR+mSocketId)); - mReceiver.setReceiveBufferSize(500000); - mReceiver.setSoTimeout(3000); - mSender = mLss.accept(); - mSender.setSendBufferSize(500000); - } - - protected void closeSockets() { - try { - mReceiver.close(); - } catch (Exception e) { - e.printStackTrace(); - } - try { - mSender.close(); - } catch (Exception e) { - e.printStackTrace(); - } - try { - mLss.close(); - } catch (Exception e) { - e.printStackTrace(); - } - mLss = null; - mSender = null; - mReceiver = null; - } - -} diff --git a/src/net/majorkernelpanic/streaming/Session.java b/src/net/majorkernelpanic/streaming/Session.java deleted file mode 100644 index a904fd4f..00000000 --- a/src/net/majorkernelpanic/streaming/Session.java +++ /dev/null @@ -1,745 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.concurrent.CountDownLatch; - -import net.majorkernelpanic.streaming.audio.AudioQuality; -import net.majorkernelpanic.streaming.audio.AudioStream; -import net.majorkernelpanic.streaming.exceptions.CameraInUseException; -import net.majorkernelpanic.streaming.exceptions.ConfNotSupportedException; -import net.majorkernelpanic.streaming.exceptions.InvalidSurfaceException; -import net.majorkernelpanic.streaming.exceptions.StorageUnavailableException; -import net.majorkernelpanic.streaming.gl.SurfaceView; -import net.majorkernelpanic.streaming.rtsp.RtspClient; -import net.majorkernelpanic.streaming.video.VideoQuality; -import net.majorkernelpanic.streaming.video.VideoStream; -import android.hardware.Camera.CameraInfo; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; - -/** - * You should instantiate this class with the {@link SessionBuilder}.
- * This is the class you will want to use to stream audio and or video to some peer using RTP.
- * - * It holds a {@link VideoStream} and a {@link AudioStream} together and provides - * synchronous and asynchronous functions to start and stop those steams. - * You should implement a callback interface {@link Callback} to receive notifications and error reports.
- * - * If you want to stream to a RTSP server, you will need an instance of this class and hand it to a {@link RtspClient}. - * - * If you don't use the RTSP protocol, you will still need to send a session description to the receiver - * for him to be able to decode your audio/video streams. You can obtain this session description by calling - * {@link #configure()} or {@link #syncConfigure()} to configure the session with its parameters - * (audio samplingrate, video resolution) and then {@link Session#getSessionDescription()}.
- * - * See the example 2 here: https://github.com/fyhertz/libstreaming-examples to - * see an example of how to get a SDP.
- * - * See the example 3 here: https://github.com/fyhertz/libstreaming-examples to - * see an example of how to stream to a RTSP server.
- * - */ -public class Session { - - public final static String TAG = "Session"; - - public final static int STREAM_VIDEO = 0x01; - - public final static int STREAM_AUDIO = 0x00; - - /** Some app is already using a camera (Camera.open() has failed). */ - public final static int ERROR_CAMERA_ALREADY_IN_USE = 0x00; - - /** The phone may not support some streaming parameters that you are trying to use (bit rate, frame rate, resolution...). */ - public final static int ERROR_CONFIGURATION_NOT_SUPPORTED = 0x01; - - /** - * The internal storage of the phone is not ready. - * libstreaming tried to store a test file on the sdcard but couldn't. - * See H264Stream and AACStream to find out why libstreaming would want to something like that. - */ - public final static int ERROR_STORAGE_NOT_READY = 0x02; - - /** The phone has no flash. */ - public final static int ERROR_CAMERA_HAS_NO_FLASH = 0x03; - - /** The supplied SurfaceView is not a valid surface, or has not been created yet. */ - public final static int ERROR_INVALID_SURFACE = 0x04; - - /** - * The destination set with {@link Session#setDestination(String)} could not be resolved. - * May mean that the phone has no access to the internet, or that the DNS server could not - * resolved the host name. - */ - public final static int ERROR_UNKNOWN_HOST = 0x05; - - /** - * Some other error occurred ! - */ - public final static int ERROR_OTHER = 0x06; - - private String mOrigin; - private String mDestination; - private int mTimeToLive = 64; - private long mTimestamp; - - private AudioStream mAudioStream = null; - private VideoStream mVideoStream = null; - - private Callback mCallback; - private Handler mMainHandler; - - private Handler mHandler; - - /** - * Creates a streaming session that can be customized by adding tracks. - */ - public Session() { - long uptime = System.currentTimeMillis(); - - HandlerThread thread = new HandlerThread("net.majorkernelpanic.streaming.Session"); - thread.start(); - - mHandler = new Handler(thread.getLooper()); - mMainHandler = new Handler(Looper.getMainLooper()); - mTimestamp = (uptime/1000)<<32 & (((uptime-((uptime/1000)*1000))>>32)/1000); // NTP timestamp - mOrigin = "127.0.0.1"; - } - - /** - * The callback interface you need to implement to get some feedback - * Those will be called from the UI thread. - */ - public interface Callback { - - /** - * Called periodically to inform you on the bandwidth - * consumption of the streams when streaming. - */ - public void onBitrateUpdate(long bitrate); - - /** Called when some error occurs. */ - public void onSessionError(int reason, int streamType, Exception e); - - /** - * Called when the previw of the {@link VideoStream} - * has correctly been started. - * If an error occurs while starting the preview, - * {@link Callback#onSessionError(int, int, Exception)} will be - * called instead of {@link Callback#onPreviewStarted()}. - */ - public void onPreviewStarted(); - - /** - * Called when the session has correctly been configured - * after calling {@link Session#configure()}. - * If an error occurs while configuring the {@link Session}, - * {@link Callback#onSessionError(int, int, Exception)} will be - * called instead of {@link Callback#onSessionConfigured()}. - */ - public void onSessionConfigured(); - - /** - * Called when the streams of the session have correctly been started. - * If an error occurs while starting the {@link Session}, - * {@link Callback#onSessionError(int, int, Exception)} will be - * called instead of {@link Callback#onSessionStarted()}. - */ - public void onSessionStarted(); - - /** Called when the stream of the session have been stopped. */ - public void onSessionStopped(); - - } - - /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ - void addAudioTrack(AudioStream track) { - removeAudioTrack(); - mAudioStream = track; - } - - /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ - void addVideoTrack(VideoStream track) { - removeVideoTrack(); - mVideoStream = track; - } - - /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ - void removeAudioTrack() { - if (mAudioStream != null) { - mAudioStream.stop(); - mAudioStream = null; - } - } - - /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ - void removeVideoTrack() { - if (mVideoStream != null) { - mVideoStream.stopPreview(); - mVideoStream = null; - } - } - - /** Returns the underlying {@link AudioStream} used by the {@link Session}. */ - public AudioStream getAudioTrack() { - return mAudioStream; - } - - /** Returns the underlying {@link VideoStream} used by the {@link Session}. */ - public VideoStream getVideoTrack() { - return mVideoStream; - } - - /** - * Sets the callback interface that will be called by the {@link Session}. - * @param callback The implementation of the {@link Callback} interface - */ - public void setCallback(Callback callback) { - mCallback = callback; - } - - /** - * The origin address of the session. - * It appears in the session description. - * @param origin The origin address - */ - public void setOrigin(String origin) { - mOrigin = origin; - } - - /** - * The destination address for all the streams of the session.
- * Changes will be taken into account the next time you start the session. - * @param destination The destination address - */ - public void setDestination(String destination) { - mDestination = destination; - } - - /** - * Set the TTL of all packets sent during the session.
- * Changes will be taken into account the next time you start the session. - * @param ttl The Time To Live - */ - public void setTimeToLive(int ttl) { - mTimeToLive = ttl; - } - - /** - * Sets the configuration of the stream.
- * You can call this method at any time and changes will take - * effect next time you call {@link #configure()}. - * @param quality Quality of the stream - */ - public void setVideoQuality(VideoQuality quality) { - if (mVideoStream != null) { - mVideoStream.setVideoQuality(quality); - } - } - - /** - * Sets a Surface to show a preview of recorded media (video).
- * You can call this method at any time and changes will take - * effect next time you call {@link #start()} or {@link #startPreview()}. - */ - public void setSurfaceView(final SurfaceView view) { - mHandler.post(new Runnable() { - @Override - public void run() { - if (mVideoStream != null) { - mVideoStream.setSurfaceView(view); - } - } - }); - } - - /** - * Sets the orientation of the preview.
- * You can call this method at any time and changes will take - * effect next time you call {@link #configure()}. - * @param orientation The orientation of the preview - */ - public void setPreviewOrientation(int orientation) { - if (mVideoStream != null) { - mVideoStream.setPreviewOrientation(orientation); - } - } - - /** - * Sets the configuration of the stream.
- * You can call this method at any time and changes will take - * effect next time you call {@link #configure()}. - * @param quality Quality of the stream - */ - public void setAudioQuality(AudioQuality quality) { - if (mAudioStream != null) { - mAudioStream.setAudioQuality(quality); - } - } - - /** - * Returns the {@link Callback} interface that was set with - * {@link #setCallback(Callback)} or null if none was set. - */ - public Callback getCallback() { - return mCallback; - } - - /** - * Returns a Session Description that can be stored in a file or sent to a client with RTSP. - * @return The Session Description. - * @throws IllegalStateException Thrown when {@link #setDestination(String)} has never been called. - */ - public String getSessionDescription() { - StringBuilder sessionDescription = new StringBuilder(); - if (mDestination==null) { - throw new IllegalStateException("setDestination() has not been called !"); - } - sessionDescription.append("v=0\r\n"); - // TODO: Add IPV6 support - sessionDescription.append("o=- "+mTimestamp+" "+mTimestamp+" IN IP4 "+mOrigin+"\r\n"); - sessionDescription.append("s=Unnamed\r\n"); - sessionDescription.append("i=N/A\r\n"); - sessionDescription.append("c=IN IP4 "+mDestination+"\r\n"); - // t=0 0 means the session is permanent (we don't know when it will stop) - sessionDescription.append("t=0 0\r\n"); - sessionDescription.append("a=recvonly\r\n"); - // Prevents two different sessions from using the same peripheral at the same time - if (mAudioStream != null) { - sessionDescription.append(mAudioStream.getSessionDescription()); - sessionDescription.append("a=control:trackID="+0+"\r\n"); - } - if (mVideoStream != null) { - sessionDescription.append(mVideoStream.getSessionDescription()); - sessionDescription.append("a=control:trackID="+1+"\r\n"); - } - return sessionDescription.toString(); - } - - /** Returns the destination set with {@link #setDestination(String)}. */ - public String getDestination() { - return mDestination; - } - - /** Returns an approximation of the bandwidth consumed by the session in bit per second. */ - public long getBitrate() { - long sum = 0; - if (mAudioStream != null) sum += mAudioStream.getBitrate(); - if (mVideoStream != null) sum += mVideoStream.getBitrate(); - return sum; - } - - /** Indicates if a track is currently running. */ - public boolean isStreaming() { - if ( (mAudioStream!=null && mAudioStream.isStreaming()) || (mVideoStream!=null && mVideoStream.isStreaming()) ) - return true; - else - return false; - } - - /** - * Configures all streams of the session. - **/ - public void configure() { - mHandler.post(new Runnable() { - @Override - public void run() { - try { - syncConfigure(); - } catch (Exception e) {}; - } - }); - } - - /** - * Does the same thing as {@link #configure()}, but in a synchronous manner.
- * Throws exceptions in addition to calling a callback - * {@link Callback#onSessionError(int, int, Exception)} when - * an error occurs. - **/ - public void syncConfigure() - throws CameraInUseException, - StorageUnavailableException, - ConfNotSupportedException, - InvalidSurfaceException, - RuntimeException, - IOException { - - for (int id=0;id<2;id++) { - Stream stream = id==0 ? mAudioStream : mVideoStream; - if (stream!=null && !stream.isStreaming()) { - try { - stream.configure(); - } catch (CameraInUseException e) { - postError(ERROR_CAMERA_ALREADY_IN_USE , id, e); - throw e; - } catch (StorageUnavailableException e) { - postError(ERROR_STORAGE_NOT_READY , id, e); - throw e; - } catch (ConfNotSupportedException e) { - postError(ERROR_CONFIGURATION_NOT_SUPPORTED , id, e); - throw e; - } catch (InvalidSurfaceException e) { - postError(ERROR_INVALID_SURFACE , id, e); - throw e; - } catch (IOException e) { - postError(ERROR_OTHER, id, e); - throw e; - } catch (RuntimeException e) { - postError(ERROR_OTHER, id, e); - throw e; - } - } - } - postSessionConfigured(); - } - - /** - * Asynchronously starts all streams of the session. - **/ - public void start() { - mHandler.post(new Runnable() { - @Override - public void run() { - try { - syncStart(); - } catch (Exception e) {} - } - }); - } - - /** - * Starts a stream in a synchronous manner.
- * Throws exceptions in addition to calling a callback. - * @param id The id of the stream to start - **/ - public void syncStart(int id) - throws CameraInUseException, - StorageUnavailableException, - ConfNotSupportedException, - InvalidSurfaceException, - UnknownHostException, - IOException { - - Stream stream = id==0 ? mAudioStream : mVideoStream; - if (stream!=null && !stream.isStreaming()) { - try { - InetAddress destination = InetAddress.getByName(mDestination); - stream.setTimeToLive(mTimeToLive); - stream.setDestinationAddress(destination); - stream.start(); - if (getTrack(1-id) == null || getTrack(1-id).isStreaming()) { - postSessionStarted(); - } - if (getTrack(1-id) == null || !getTrack(1-id).isStreaming()) { - mHandler.post(mUpdateBitrate); - } - } catch (UnknownHostException e) { - postError(ERROR_UNKNOWN_HOST, id, e); - throw e; - } catch (CameraInUseException e) { - postError(ERROR_CAMERA_ALREADY_IN_USE , id, e); - throw e; - } catch (StorageUnavailableException e) { - postError(ERROR_STORAGE_NOT_READY , id, e); - throw e; - } catch (ConfNotSupportedException e) { - postError(ERROR_CONFIGURATION_NOT_SUPPORTED , id, e); - throw e; - } catch (InvalidSurfaceException e) { - postError(ERROR_INVALID_SURFACE , id, e); - throw e; - } catch (IOException e) { - postError(ERROR_OTHER, id, e); - throw e; - } catch (RuntimeException e) { - postError(ERROR_OTHER, id, e); - throw e; - } - } - - } - - /** - * Does the same thing as {@link #start()}, but in a synchronous manner.
- * Throws exceptions in addition to calling a callback. - **/ - public void syncStart() - throws CameraInUseException, - StorageUnavailableException, - ConfNotSupportedException, - InvalidSurfaceException, - UnknownHostException, - IOException { - - syncStart(1); - try { - syncStart(0); - } catch (RuntimeException e) { - syncStop(1); - throw e; - } catch (IOException e) { - syncStop(1); - throw e; - } - - } - - /** Stops all existing streams. */ - public void stop() { - mHandler.post(new Runnable() { - @Override - public void run() { - syncStop(); - } - }); - } - - /** - * Stops one stream in a synchronous manner. - * @param id The id of the stream to stop - **/ - private void syncStop(final int id) { - Stream stream = id==0 ? mAudioStream : mVideoStream; - if (stream!=null) { - stream.stop(); - } - } - - /** Stops all existing streams in a synchronous manner. */ - public void syncStop() { - syncStop(0); - syncStop(1); - postSessionStopped(); - } - - /** - * Asynchronously starts the camera preview.
- * You should of course pass a {@link SurfaceView} to {@link #setSurfaceView(SurfaceView)} - * before calling this method. Otherwise, the {@link Callback#onSessionError(int, int, Exception)} - * callback will be called with {@link #ERROR_INVALID_SURFACE}. - */ - public void startPreview() { - mHandler.post(new Runnable() { - @Override - public void run() { - if (mVideoStream != null) { - try { - mVideoStream.startPreview(); - postPreviewStarted(); - mVideoStream.configure(); - } catch (CameraInUseException e) { - postError(ERROR_CAMERA_ALREADY_IN_USE , STREAM_VIDEO, e); - } catch (ConfNotSupportedException e) { - postError(ERROR_CONFIGURATION_NOT_SUPPORTED , STREAM_VIDEO, e); - } catch (InvalidSurfaceException e) { - postError(ERROR_INVALID_SURFACE , STREAM_VIDEO, e); - } catch (RuntimeException e) { - postError(ERROR_OTHER, STREAM_VIDEO, e); - } catch (StorageUnavailableException e) { - postError(ERROR_STORAGE_NOT_READY, STREAM_VIDEO, e); - } catch (IOException e) { - postError(ERROR_OTHER, STREAM_VIDEO, e); - } - } - } - }); - } - - /** - * Asynchronously stops the camera preview. - */ - public void stopPreview() { - mHandler.post(new Runnable() { - @Override - public void run() { - if (mVideoStream != null) { - mVideoStream.stopPreview(); - } - } - }); - } - - /** Switch between the front facing and the back facing camera of the phone.
- * If {@link #startPreview()} has been called, the preview will be briefly interrupted.
- * If {@link #start()} has been called, the stream will be briefly interrupted.
- * To find out which camera is currently selected, use {@link #getCamera()} - **/ - public void switchCamera() { - mHandler.post(new Runnable() { - @Override - public void run() { - if (mVideoStream != null) { - try { - mVideoStream.switchCamera(); - postPreviewStarted(); - } catch (CameraInUseException e) { - postError(ERROR_CAMERA_ALREADY_IN_USE , STREAM_VIDEO, e); - } catch (ConfNotSupportedException e) { - postError(ERROR_CONFIGURATION_NOT_SUPPORTED , STREAM_VIDEO, e); - } catch (InvalidSurfaceException e) { - postError(ERROR_INVALID_SURFACE , STREAM_VIDEO, e); - } catch (IOException e) { - postError(ERROR_OTHER, STREAM_VIDEO, e); - } catch (RuntimeException e) { - postError(ERROR_OTHER, STREAM_VIDEO, e); - } - } - } - }); - } - - /** - * Returns the id of the camera currently selected.
- * It can be either {@link CameraInfo#CAMERA_FACING_BACK} or - * {@link CameraInfo#CAMERA_FACING_FRONT}. - */ - public int getCamera() { - return mVideoStream != null ? mVideoStream.getCamera() : 0; - - } - - /** - * Toggles the LED of the phone if it has one. - * You can get the current state of the flash with - * {@link Session#getVideoTrack()} and {@link VideoStream#getFlashState()}. - **/ - public void toggleFlash() { - mHandler.post(new Runnable() { - @Override - public void run() { - if (mVideoStream != null) { - try { - mVideoStream.toggleFlash(); - } catch (RuntimeException e) { - postError(ERROR_CAMERA_HAS_NO_FLASH, STREAM_VIDEO, e); - } - } - } - }); - } - - /** Deletes all existing tracks & release associated resources. */ - public void release() { - removeAudioTrack(); - removeVideoTrack(); - mHandler.getLooper().quit(); - } - - private void postPreviewStarted() { - mMainHandler.post(new Runnable() { - @Override - public void run() { - if (mCallback != null) { - mCallback.onPreviewStarted(); - } - } - }); - } - - private void postSessionConfigured() { - mMainHandler.post(new Runnable() { - @Override - public void run() { - if (mCallback != null) { - mCallback.onSessionConfigured(); - } - } - }); - } - - private void postSessionStarted() { - mMainHandler.post(new Runnable() { - @Override - public void run() { - if (mCallback != null) { - mCallback.onSessionStarted(); - } - } - }); - } - - private void postSessionStopped() { - mMainHandler.post(new Runnable() { - @Override - public void run() { - if (mCallback != null) { - mCallback.onSessionStopped(); - } - } - }); - } - - private void postError(final int reason, final int streamType,final Exception e) { - mMainHandler.post(new Runnable() { - @Override - public void run() { - if (mCallback != null) { - mCallback.onSessionError(reason, streamType, e); - } - } - }); - } - - private void postBitRate(final long bitrate) { - mMainHandler.post(new Runnable() { - @Override - public void run() { - if (mCallback != null) { - mCallback.onBitrateUpdate(bitrate); - } - } - }); - } - - private Runnable mUpdateBitrate = new Runnable() { - @Override - public void run() { - if (isStreaming()) { - postBitRate(getBitrate()); - mHandler.postDelayed(mUpdateBitrate, 500); - } else { - postBitRate(0); - } - } - }; - - - public boolean trackExists(int id) { - if (id==0) - return mAudioStream!=null; - else - return mVideoStream!=null; - } - - public Stream getTrack(int id) { - if (id==0) - return mAudioStream; - else - return mVideoStream; - } - -} diff --git a/src/net/majorkernelpanic/streaming/SessionBuilder.java b/src/net/majorkernelpanic/streaming/SessionBuilder.java deleted file mode 100644 index 1ba39c41..00000000 --- a/src/net/majorkernelpanic/streaming/SessionBuilder.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming; - -import java.io.IOException; -import java.net.InetAddress; - -import net.majorkernelpanic.streaming.audio.AACStream; -import net.majorkernelpanic.streaming.audio.AMRNBStream; -import net.majorkernelpanic.streaming.audio.AudioQuality; -import net.majorkernelpanic.streaming.audio.AudioStream; -import net.majorkernelpanic.streaming.gl.SurfaceView; -import net.majorkernelpanic.streaming.video.H263Stream; -import net.majorkernelpanic.streaming.video.H264Stream; -import net.majorkernelpanic.streaming.video.VideoQuality; -import net.majorkernelpanic.streaming.video.VideoStream; -import android.content.Context; -import android.hardware.Camera.CameraInfo; -import android.preference.PreferenceManager; - -/** - * Call {@link #getInstance()} to get access to the SessionBuilder. - */ -public class SessionBuilder { - - public final static String TAG = "SessionBuilder"; - - /** Can be used with {@link #setVideoEncoder}. */ - public final static int VIDEO_NONE = 0; - - /** Can be used with {@link #setVideoEncoder}. */ - public final static int VIDEO_H264 = 1; - - /** Can be used with {@link #setVideoEncoder}. */ - public final static int VIDEO_H263 = 2; - - /** Can be used with {@link #setAudioEncoder}. */ - public final static int AUDIO_NONE = 0; - - /** Can be used with {@link #setAudioEncoder}. */ - public final static int AUDIO_AMRNB = 3; - - /** Can be used with {@link #setAudioEncoder}. */ - public final static int AUDIO_AAC = 5; - - // Default configuration - private VideoQuality mVideoQuality = VideoQuality.DEFAULT_VIDEO_QUALITY; - private AudioQuality mAudioQuality = AudioQuality.DEFAULT_AUDIO_QUALITY; - private Context mContext; - private int mVideoEncoder = VIDEO_H263; - private int mAudioEncoder = AUDIO_AMRNB; - private int mCamera = CameraInfo.CAMERA_FACING_BACK; - private int mTimeToLive = 64; - private int mOrientation = 0; - private boolean mFlash = false; - private SurfaceView mSurfaceView = null; - private String mOrigin = null; - private String mDestination = null; - private Session.Callback mCallback = null; - - // Removes the default public constructor - private SessionBuilder() {} - - // The SessionManager implements the singleton pattern - private static volatile SessionBuilder sInstance = null; - - /** - * Returns a reference to the {@link SessionBuilder}. - * @return The reference to the {@link SessionBuilder} - */ - public final static SessionBuilder getInstance() { - if (sInstance == null) { - synchronized (SessionBuilder.class) { - if (sInstance == null) { - SessionBuilder.sInstance = new SessionBuilder(); - } - } - } - return sInstance; - } - - /** - * Creates a new {@link Session}. - * @return The new Session - * @throws IOException - */ - public Session build() { - Session session; - - session = new Session(); - session.setOrigin(mOrigin); - session.setDestination(mDestination); - session.setTimeToLive(mTimeToLive); - session.setCallback(mCallback); - - switch (mAudioEncoder) { - case AUDIO_AAC: - AACStream stream = new AACStream(); - session.addAudioTrack(stream); - if (mContext!=null) - stream.setPreferences(PreferenceManager.getDefaultSharedPreferences(mContext)); - break; - case AUDIO_AMRNB: - session.addAudioTrack(new AMRNBStream()); - break; - } - - switch (mVideoEncoder) { - case VIDEO_H263: - session.addVideoTrack(new H263Stream(mCamera)); - break; - case VIDEO_H264: - H264Stream stream = new H264Stream(mCamera); - if (mContext!=null) - stream.setPreferences(PreferenceManager.getDefaultSharedPreferences(mContext)); - session.addVideoTrack(stream); - break; - } - - if (session.getVideoTrack()!=null) { - VideoStream video = session.getVideoTrack(); - video.setFlashState(mFlash); - video.setVideoQuality(mVideoQuality); - video.setSurfaceView(mSurfaceView); - video.setPreviewOrientation(mOrientation); - video.setDestinationPorts(5006); - } - - if (session.getAudioTrack()!=null) { - AudioStream audio = session.getAudioTrack(); - audio.setAudioQuality(mAudioQuality); - audio.setDestinationPorts(5004); - } - - return session; - - } - - /** - * Access to the context is needed for the H264Stream class to store some stuff in the SharedPreferences. - * Note that you should pass the Application context, not the context of an Activity. - **/ - public SessionBuilder setContext(Context context) { - mContext = context; - return this; - } - - /** Sets the destination of the session. */ - public SessionBuilder setDestination(String destination) { - mDestination = destination; - return this; - } - - /** Sets the origin of the session. It appears in the SDP of the session. */ - public SessionBuilder setOrigin(String origin) { - mOrigin = origin; - return this; - } - - /** Sets the video stream quality. */ - public SessionBuilder setVideoQuality(VideoQuality quality) { - mVideoQuality = quality.clone(); - return this; - } - - /** Sets the audio encoder. */ - public SessionBuilder setAudioEncoder(int encoder) { - mAudioEncoder = encoder; - return this; - } - - /** Sets the audio quality. */ - public SessionBuilder setAudioQuality(AudioQuality quality) { - mAudioQuality = quality.clone(); - return this; - } - - /** Sets the default video encoder. */ - public SessionBuilder setVideoEncoder(int encoder) { - mVideoEncoder = encoder; - return this; - } - - public SessionBuilder setFlashEnabled(boolean enabled) { - mFlash = enabled; - return this; - } - - public SessionBuilder setCamera(int camera) { - mCamera = camera; - return this; - } - - public SessionBuilder setTimeToLive(int ttl) { - mTimeToLive = ttl; - return this; - } - - /** - * Sets the SurfaceView required to preview the video stream. - **/ - public SessionBuilder setSurfaceView(SurfaceView surfaceView) { - mSurfaceView = surfaceView; - return this; - } - - /** - * Sets the orientation of the preview. - * @param orientation The orientation of the preview - */ - public SessionBuilder setPreviewOrientation(int orientation) { - mOrientation = orientation; - return this; - } - - public SessionBuilder setCallback(Session.Callback callback) { - mCallback = callback; - return this; - } - - /** Returns the context set with {@link #setContext(Context)}*/ - public Context getContext() { - return mContext; - } - - /** Returns the destination ip address set with {@link #setDestination(String)}. */ - public String getDestination() { - return mDestination; - } - - /** Returns the origin ip address set with {@link #setOrigin(String)}. */ - public String getOrigin() { - return mOrigin; - } - - /** Returns the audio encoder set with {@link #setAudioEncoder(int)}. */ - public int getAudioEncoder() { - return mAudioEncoder; - } - - /** Returns the id of the {@link android.hardware.Camera} set with {@link #setCamera(int)}. */ - public int getCamera() { - return mCamera; - } - - /** Returns the video encoder set with {@link #setVideoEncoder(int)}. */ - public int getVideoEncoder() { - return mVideoEncoder; - } - - /** Returns the VideoQuality set with {@link #setVideoQuality(VideoQuality)}. */ - public VideoQuality getVideoQuality() { - return mVideoQuality; - } - - /** Returns the AudioQuality set with {@link #setAudioQuality(AudioQuality)}. */ - public AudioQuality getAudioQuality() { - return mAudioQuality; - } - - /** Returns the flash state set with {@link #setFlashEnabled(boolean)}. */ - public boolean getFlashState() { - return mFlash; - } - - /** Returns the SurfaceView set with {@link #setSurfaceView(SurfaceView)}. */ - public SurfaceView getSurfaceView() { - return mSurfaceView; - } - - - /** Returns the time to live set with {@link #setTimeToLive(int)}. */ - public int getTimeToLive() { - return mTimeToLive; - } - - /** Returns a new {@link SessionBuilder} with the same configuration. */ - public SessionBuilder clone() { - return new SessionBuilder() - .setDestination(mDestination) - .setOrigin(mOrigin) - .setSurfaceView(mSurfaceView) - .setPreviewOrientation(mOrientation) - .setVideoQuality(mVideoQuality) - .setVideoEncoder(mVideoEncoder) - .setFlashEnabled(mFlash) - .setCamera(mCamera) - .setTimeToLive(mTimeToLive) - .setAudioEncoder(mAudioEncoder) - .setAudioQuality(mAudioQuality) - .setContext(mContext) - .setCallback(mCallback); - } - -} \ No newline at end of file diff --git a/src/net/majorkernelpanic/streaming/Stream.java b/src/net/majorkernelpanic/streaming/Stream.java deleted file mode 100644 index 57cd267a..00000000 --- a/src/net/majorkernelpanic/streaming/Stream.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetAddress; - -/** - * An interface that represents a Stream. - */ -public interface Stream { - - /** - * Configures the stream. You need to call this before calling {@link #getSessionDescription()} - * to apply your configuration of the stream. - */ - public void configure() throws IllegalStateException, IOException; - - /** - * Starts the stream. - * This method can only be called after {@link Stream#configure()}. - */ - public void start() throws IllegalStateException, IOException; - - /** - * Stops the stream. - */ - public void stop(); - - /** - * Sets the Time To Live of packets sent over the network. - * @param ttl The time to live - * @throws IOException - */ - public void setTimeToLive(int ttl) throws IOException; - - /** - * Sets the destination ip address of the stream. - * @param dest The destination address of the stream - */ - public void setDestinationAddress(InetAddress dest); - - /** - * Sets the destination ports of the stream. - * If an odd number is supplied for the destination port then the next - * lower even number will be used for RTP and it will be used for RTCP. - * If an even number is supplied, it will be used for RTP and the next odd - * number will be used for RTCP. - * @param dport The destination port - */ - public void setDestinationPorts(int dport); - - /** - * Sets the destination ports of the stream. - * @param rtpPort Destination port that will be used for RTP - * @param rtcpPort Destination port that will be used for RTCP - */ - public void setDestinationPorts(int rtpPort, int rtcpPort); - - /** - * If a TCP is used as the transport protocol for the RTP session, - * the output stream to which RTP packets will be written to must - * be specified with this method. - */ - public void setOutputStream(OutputStream stream, byte channelIdentifier); - - /** - * Returns a pair of source ports, the first one is the - * one used for RTP and the second one is used for RTCP. - **/ - public int[] getLocalPorts(); - - /** - * Returns a pair of destination ports, the first one is the - * one used for RTP and the second one is used for RTCP. - **/ - public int[] getDestinationPorts(); - - - /** - * Returns the SSRC of the underlying {@link net.majorkernelpanic.streaming.rtp.RtpSocket}. - * @return the SSRC of the stream. - */ - public int getSSRC(); - - /** - * Returns an approximation of the bit rate consumed by the stream in bit per seconde. - */ - public long getBitrate(); - - /** - * Returns a description of the stream using SDP. - * This method can only be called after {@link Stream#configure()}. - * @throws IllegalStateException Thrown when {@link Stream#configure()} wa not called. - */ - public String getSessionDescription() throws IllegalStateException; - - public boolean isStreaming(); - -} diff --git a/src/net/majorkernelpanic/streaming/audio/AACStream.java b/src/net/majorkernelpanic/streaming/audio/AACStream.java deleted file mode 100644 index 655cb803..00000000 --- a/src/net/majorkernelpanic/streaming/audio/AACStream.java +++ /dev/null @@ -1,377 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.audio; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.lang.reflect.Field; -import java.net.InetAddress; -import java.nio.ByteBuffer; - -import net.majorkernelpanic.streaming.SessionBuilder; -import net.majorkernelpanic.streaming.rtp.AACADTSPacketizer; -import net.majorkernelpanic.streaming.rtp.AACLATMPacketizer; -import net.majorkernelpanic.streaming.rtp.MediaCodecInputStream; -import android.annotation.SuppressLint; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.media.AudioFormat; -import android.media.AudioRecord; -import android.media.MediaCodec; -import android.media.MediaCodecInfo; -import android.media.MediaFormat; -import android.media.MediaRecorder; -import android.os.Build; -import android.os.Environment; -import android.service.textservice.SpellCheckerService.Session; -import android.util.Log; - -/** - * A class for streaming AAC from the camera of an android device using RTP. - * You should use a {@link Session} instantiated with {@link SessionBuilder} instead of using this class directly. - * Call {@link #setDestinationAddress(InetAddress)}, {@link #setDestinationPorts(int)} and {@link #setAudioQuality(AudioQuality)} - * to configure the stream. You can then call {@link #start()} to start the RTP stream. - * Call {@link #stop()} to stop the stream. - */ -public class AACStream extends AudioStream { - - public final static String TAG = "AACStream"; - - /** MPEG-4 Audio Object Types supported by ADTS. **/ - private static final String[] AUDIO_OBJECT_TYPES = { - "NULL", // 0 - "AAC Main", // 1 - "AAC LC (Low Complexity)", // 2 - "AAC SSR (Scalable Sample Rate)", // 3 - "AAC LTP (Long Term Prediction)" // 4 - }; - - /** There are 13 supported frequencies by ADTS. **/ - public static final int[] AUDIO_SAMPLING_RATES = { - 96000, // 0 - 88200, // 1 - 64000, // 2 - 48000, // 3 - 44100, // 4 - 32000, // 5 - 24000, // 6 - 22050, // 7 - 16000, // 8 - 12000, // 9 - 11025, // 10 - 8000, // 11 - 7350, // 12 - -1, // 13 - -1, // 14 - -1, // 15 - }; - - private String mSessionDescription = null; - private int mProfile, mSamplingRateIndex, mChannel, mConfig; - private SharedPreferences mSettings = null; - private AudioRecord mAudioRecord = null; - private Thread mThread = null; - - public AACStream() { - super(); - - if (!AACStreamingSupported()) { - Log.e(TAG,"AAC not supported on this phone"); - throw new RuntimeException("AAC not supported by this phone !"); - } else { - Log.d(TAG,"AAC supported on this phone"); - } - - } - - private static boolean AACStreamingSupported() { - if (Build.VERSION.SDK_INT<14) return false; - try { - MediaRecorder.OutputFormat.class.getField("AAC_ADTS"); - return true; - } catch (Exception e) { - return false; - } - } - - /** - * Some data (the actual sampling rate used by the phone and the AAC profile) needs to be stored once {@link #getSessionDescription()} is called. - * @param prefs The SharedPreferences that will be used to store the sampling rate - */ - public void setPreferences(SharedPreferences prefs) { - mSettings = prefs; - } - - @Override - public synchronized void start() throws IllegalStateException, IOException { - if (!mStreaming) { - configure(); - super.start(); - } - } - - public synchronized void configure() throws IllegalStateException, IOException { - super.configure(); - mQuality = mRequestedQuality.clone(); - - // Checks if the user has supplied an exotic sampling rate - int i=0; - for (;i12) mQuality.samplingRate = 16000; - - if (mMode != mRequestedMode || mPacketizer==null) { - mMode = mRequestedMode; - if (mMode == MODE_MEDIARECORDER_API) { - mPacketizer = new AACADTSPacketizer(); - } else { - mPacketizer = new AACLATMPacketizer(); - } - mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort); - mPacketizer.getRtpSocket().setOutputStream(mOutputStream, mChannelIdentifier); - } - - if (mMode == MODE_MEDIARECORDER_API) { - - testADTS(); - - // All the MIME types parameters used here are described in RFC 3640 - // SizeLength: 13 bits will be enough because ADTS uses 13 bits for frame length - // config: contains the object type + the sampling rate + the channel number - - // TODO: streamType always 5 ? profile-level-id always 15 ? - - mSessionDescription = "m=audio "+String.valueOf(getDestinationPorts()[0])+" RTP/AVP 96\r\n" + - "a=rtpmap:96 mpeg4-generic/"+mQuality.samplingRate+"\r\n"+ - "a=fmtp:96 streamtype=5; profile-level-id=15; mode=AAC-hbr; config="+Integer.toHexString(mConfig)+"; SizeLength=13; IndexLength=3; IndexDeltaLength=3;\r\n"; - - } else { - - mProfile = 2; // AAC LC - mChannel = 1; - mConfig = (mProfile & 0x1F) << 11 | (mSamplingRateIndex & 0x0F) << 7 | (mChannel & 0x0F) << 3; - - mSessionDescription = "m=audio "+String.valueOf(getDestinationPorts()[0])+" RTP/AVP 96\r\n" + - "a=rtpmap:96 mpeg4-generic/"+mQuality.samplingRate+"\r\n"+ - "a=fmtp:96 streamtype=5; profile-level-id=15; mode=AAC-hbr; config="+Integer.toHexString(mConfig)+"; SizeLength=13; IndexLength=3; IndexDeltaLength=3;\r\n"; - - } - - } - - @Override - protected void encodeWithMediaRecorder() throws IOException { - testADTS(); - ((AACADTSPacketizer)mPacketizer).setSamplingRate(mQuality.samplingRate); - super.encodeWithMediaRecorder(); - } - - @Override - @SuppressLint({ "InlinedApi", "NewApi" }) - protected void encodeWithMediaCodec() throws IOException { - - final int bufferSize = AudioRecord.getMinBufferSize(mQuality.samplingRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)*2; - - ((AACLATMPacketizer)mPacketizer).setSamplingRate(mQuality.samplingRate); - - mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, mQuality.samplingRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); - mMediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm"); - MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm"); - format.setInteger(MediaFormat.KEY_BIT_RATE, mQuality.bitRate); - format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); - format.setInteger(MediaFormat.KEY_SAMPLE_RATE, mQuality.samplingRate); - format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); - format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize); - mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - mAudioRecord.startRecording(); - mMediaCodec.start(); - - final MediaCodecInputStream inputStream = new MediaCodecInputStream(mMediaCodec); - final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); - - mThread = new Thread(new Runnable() { - @Override - public void run() { - int len = 0, bufferIndex = 0; - try { - while (!Thread.interrupted()) { - bufferIndex = mMediaCodec.dequeueInputBuffer(10000); - if (bufferIndex>=0) { - inputBuffers[bufferIndex].clear(); - len = mAudioRecord.read(inputBuffers[bufferIndex], bufferSize); - if (len == AudioRecord.ERROR_INVALID_OPERATION || len == AudioRecord.ERROR_BAD_VALUE) { - Log.e(TAG,"An error occured with the AudioRecord API !"); - } else { - //Log.v(TAG,"Pushing raw audio to the decoder: len="+len+" bs: "+inputBuffers[bufferIndex].capacity()); - mMediaCodec.queueInputBuffer(bufferIndex, 0, len, System.nanoTime()/1000, 0); - } - } - } - } catch (RuntimeException e) { - e.printStackTrace(); - } - } - }); - - mThread.start(); - - // The packetizer encapsulates this stream in an RTP stream and send it over the network - mPacketizer.setInputStream(inputStream); - mPacketizer.start(); - - mStreaming = true; - - } - - /** Stops the stream. */ - public synchronized void stop() { - if (mStreaming) { - if (mMode==MODE_MEDIACODEC_API) { - Log.d(TAG, "Interrupting threads..."); - mThread.interrupt(); - mAudioRecord.stop(); - mAudioRecord.release(); - mAudioRecord = null; - } - super.stop(); - } - } - - /** - * Returns a description of the stream using SDP. It can then be included in an SDP file. - * Will fail if called when streaming. - */ - public String getSessionDescription() throws IllegalStateException { - if (mSessionDescription == null) throw new IllegalStateException("You need to call configure() first !"); - return mSessionDescription; - } - - /** - * Records a short sample of AAC ADTS from the microphone to find out what the sampling rate really is - * On some phone indeed, no error will be reported if the sampling rate used differs from the - * one selected with setAudioSamplingRate - * @throws IOException - * @throws IllegalStateException - */ - @SuppressLint("InlinedApi") - private void testADTS() throws IllegalStateException, IOException { - - setAudioEncoder(MediaRecorder.AudioEncoder.AAC); - try { - Field name = MediaRecorder.OutputFormat.class.getField("AAC_ADTS"); - setOutputFormat(name.getInt(null)); - } - catch (Exception ignore) { - setOutputFormat(6); - } - - String key = PREF_PREFIX+"aac-"+mQuality.samplingRate; - - if (mSettings!=null) { - if (mSettings.contains(key)) { - String[] s = mSettings.getString(key, "").split(","); - mQuality.samplingRate = Integer.valueOf(s[0]); - mConfig = Integer.valueOf(s[1]); - mChannel = Integer.valueOf(s[2]); - return; - } - } - - final String TESTFILE = Environment.getExternalStorageDirectory().getPath()+"/spydroid-test.adts"; - - if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - throw new IllegalStateException("No external storage or external storage not ready !"); - } - - // The structure of an ADTS packet is described here: http://wiki.multimedia.cx/index.php?title=ADTS - - // ADTS header is 7 or 9 bytes long - byte[] buffer = new byte[9]; - - mMediaRecorder = new MediaRecorder(); - mMediaRecorder.setAudioSource(mAudioSource); - mMediaRecorder.setOutputFormat(mOutputFormat); - mMediaRecorder.setAudioEncoder(mAudioEncoder); - mMediaRecorder.setAudioChannels(1); - mMediaRecorder.setAudioSamplingRate(mQuality.samplingRate); - mMediaRecorder.setAudioEncodingBitRate(mQuality.bitRate); - mMediaRecorder.setOutputFile(TESTFILE); - mMediaRecorder.setMaxDuration(1000); - mMediaRecorder.prepare(); - mMediaRecorder.start(); - - // We record for 1 sec - // TODO: use the MediaRecorder.OnInfoListener - try { - Thread.sleep(2000); - } catch (InterruptedException e) {} - - mMediaRecorder.stop(); - mMediaRecorder.release(); - mMediaRecorder = null; - - File file = new File(TESTFILE); - RandomAccessFile raf = new RandomAccessFile(file, "r"); - - // ADTS packets start with a sync word: 12bits set to 1 - while (true) { - if ( (raf.readByte()&0xFF) == 0xFF ) { - buffer[0] = raf.readByte(); - if ( (buffer[0]&0xF0) == 0xF0) break; - } - } - - raf.read(buffer,1,5); - - mSamplingRateIndex = (buffer[1]&0x3C)>>2 ; - mProfile = ( (buffer[1]&0xC0) >> 6 ) + 1 ; - mChannel = (buffer[1]&0x01) << 2 | (buffer[2]&0xC0) >> 6 ; - mQuality.samplingRate = AUDIO_SAMPLING_RATES[mSamplingRateIndex]; - - // 5 bits for the object type / 4 bits for the sampling rate / 4 bits for the channel / padding - mConfig = (mProfile & 0x1F) << 11 | (mSamplingRateIndex & 0x0F) << 7 | (mChannel & 0x0F) << 3; - - Log.i(TAG,"MPEG VERSION: " + ( (buffer[0]&0x08) >> 3 ) ); - Log.i(TAG,"PROTECTION: " + (buffer[0]&0x01) ); - Log.i(TAG,"PROFILE: " + AUDIO_OBJECT_TYPES[ mProfile ] ); - Log.i(TAG,"SAMPLING FREQUENCY: " + mQuality.samplingRate ); - Log.i(TAG,"CHANNEL: " + mChannel ); - - raf.close(); - - if (mSettings!=null) { - Editor editor = mSettings.edit(); - editor.putString(key, mQuality.samplingRate+","+mConfig+","+mChannel); - editor.commit(); - } - - if (!file.delete()) Log.e(TAG,"Temp file could not be erased"); - - } - -} diff --git a/src/net/majorkernelpanic/streaming/audio/AMRNBStream.java b/src/net/majorkernelpanic/streaming/audio/AMRNBStream.java deleted file mode 100644 index 05f50ec7..00000000 --- a/src/net/majorkernelpanic/streaming/audio/AMRNBStream.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.audio; - -import java.io.IOException; -import java.lang.reflect.Field; - -import net.majorkernelpanic.streaming.SessionBuilder; -import net.majorkernelpanic.streaming.rtp.AMRNBPacketizer; -import android.media.MediaRecorder; -import android.service.textservice.SpellCheckerService.Session; - -/** - * A class for streaming AAC from the camera of an android device using RTP. - * You should use a {@link Session} instantiated with {@link SessionBuilder} instead of using this class directly. - * Call {@link #setDestinationAddress(InetAddress)}, {@link #setDestinationPorts(int)} and {@link #setAudioQuality(AudioQuality)} - * to configure the stream. You can then call {@link #start()} to start the RTP stream. - * Call {@link #stop()} to stop the stream. - */ -public class AMRNBStream extends AudioStream { - - public AMRNBStream() { - super(); - - mPacketizer = new AMRNBPacketizer(); - - setAudioSource(MediaRecorder.AudioSource.CAMCORDER); - - try { - // RAW_AMR was deprecated in API level 16. - Field deprecatedName = MediaRecorder.OutputFormat.class.getField("RAW_AMR"); - setOutputFormat(deprecatedName.getInt(null)); - } catch (Exception e) { - setOutputFormat(MediaRecorder.OutputFormat.AMR_NB); - } - - setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); - - } - - /** - * Starts the stream. - */ - public synchronized void start() throws IllegalStateException, IOException { - if (!mStreaming) { - configure(); - super.start(); - } - } - - public synchronized void configure() throws IllegalStateException, IOException { - super.configure(); - mMode = MODE_MEDIARECORDER_API; - mQuality = mRequestedQuality.clone(); - } - - /** - * Returns a description of the stream using SDP. It can then be included in an SDP file. - */ - public String getSessionDescription() { - return "m=audio "+String.valueOf(getDestinationPorts()[0])+" RTP/AVP 96\r\n" + - "a=rtpmap:96 AMR/8000\r\n" + - "a=fmtp:96 octet-align=1;\r\n"; - } - - @Override - protected void encodeWithMediaCodec() throws IOException { - super.encodeWithMediaRecorder(); - } - -} diff --git a/src/net/majorkernelpanic/streaming/audio/AudioQuality.java b/src/net/majorkernelpanic/streaming/audio/AudioQuality.java deleted file mode 100644 index 0fce0044..00000000 --- a/src/net/majorkernelpanic/streaming/audio/AudioQuality.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.audio; - -/** - * A class that represents the quality of an audio stream. - */ -public class AudioQuality { - - /** Default audio stream quality. */ - public final static AudioQuality DEFAULT_AUDIO_QUALITY = new AudioQuality(8000,32000); - - /** Represents a quality for a video stream. */ - public AudioQuality() {} - - /** - * Represents a quality for an audio stream. - * @param samplingRate The sampling rate - * @param bitRate The bitrate in bit per seconds - */ - public AudioQuality(int samplingRate, int bitRate) { - this.samplingRate = samplingRate; - this.bitRate = bitRate; - } - - public int samplingRate = 0; - public int bitRate = 0; - - public boolean equals(AudioQuality quality) { - if (quality==null) return false; - return (quality.samplingRate == this.samplingRate & - quality.bitRate == this.bitRate); - } - - public AudioQuality clone() { - return new AudioQuality(samplingRate, bitRate); - } - - public static AudioQuality parseQuality(String str) { - AudioQuality quality = DEFAULT_AUDIO_QUALITY.clone(); - if (str != null) { - String[] config = str.split("-"); - try { - quality.bitRate = Integer.parseInt(config[0])*1000; // conversion to bit/s - quality.samplingRate = Integer.parseInt(config[1]); - } - catch (IndexOutOfBoundsException ignore) {} - } - return quality; - } - -} diff --git a/src/net/majorkernelpanic/streaming/audio/AudioStream.java b/src/net/majorkernelpanic/streaming/audio/AudioStream.java deleted file mode 100644 index 9b3b2c90..00000000 --- a/src/net/majorkernelpanic/streaming/audio/AudioStream.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.audio; - -import java.io.IOException; - -import net.majorkernelpanic.streaming.MediaStream; -import android.media.MediaRecorder; -import android.util.Log; - -/** - * Don't use this class directly. - */ -public abstract class AudioStream extends MediaStream { - - protected int mAudioSource; - protected int mOutputFormat; - protected int mAudioEncoder; - protected AudioQuality mRequestedQuality = AudioQuality.DEFAULT_AUDIO_QUALITY.clone(); - protected AudioQuality mQuality = mRequestedQuality.clone(); - - public AudioStream() { - setAudioSource(MediaRecorder.AudioSource.CAMCORDER); - } - - public void setAudioSource(int audioSource) { - mAudioSource = audioSource; - } - - public void setAudioQuality(AudioQuality quality) { - mRequestedQuality = quality; - } - - /** - * Returns the quality of the stream. - */ - public AudioQuality getAudioQuality() { - return mQuality; - } - - protected void setAudioEncoder(int audioEncoder) { - mAudioEncoder = audioEncoder; - } - - protected void setOutputFormat(int outputFormat) { - mOutputFormat = outputFormat; - } - - @Override - protected void encodeWithMediaRecorder() throws IOException { - - // We need a local socket to forward data output by the camera to the packetizer - createSockets(); - - Log.v(TAG,"Requested audio with "+mQuality.bitRate/1000+"kbps"+" at "+mQuality.samplingRate/1000+"kHz"); - - mMediaRecorder = new MediaRecorder(); - mMediaRecorder.setAudioSource(mAudioSource); - mMediaRecorder.setOutputFormat(mOutputFormat); - mMediaRecorder.setAudioEncoder(mAudioEncoder); - mMediaRecorder.setAudioChannels(1); - mMediaRecorder.setAudioSamplingRate(mQuality.samplingRate); - mMediaRecorder.setAudioEncodingBitRate(mQuality.bitRate); - - // We write the ouput of the camera in a local socket instead of a file ! - // This one little trick makes streaming feasible quiet simply: data from the camera - // can then be manipulated at the other end of the socket - mMediaRecorder.setOutputFile(mSender.getFileDescriptor()); - - mMediaRecorder.prepare(); - mMediaRecorder.start(); - - try { - // mReceiver.getInputStream contains the data from the camera - // the mPacketizer encapsulates this stream in an RTP stream and send it over the network - mPacketizer.setInputStream(mReceiver.getInputStream()); - mPacketizer.start(); - mStreaming = true; - } catch (IOException e) { - stop(); - throw new IOException("Something happened with the local sockets :/ Start failed !"); - } - - } - -} diff --git a/src/net/majorkernelpanic/streaming/exceptions/CameraInUseException.java b/src/net/majorkernelpanic/streaming/exceptions/CameraInUseException.java deleted file mode 100644 index f6d19229..00000000 --- a/src/net/majorkernelpanic/streaming/exceptions/CameraInUseException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.exceptions; - -public class CameraInUseException extends RuntimeException { - - public CameraInUseException(String message) { - super(message); - } - - private static final long serialVersionUID = -1866132102949435675L; -} diff --git a/src/net/majorkernelpanic/streaming/exceptions/ConfNotSupportedException.java b/src/net/majorkernelpanic/streaming/exceptions/ConfNotSupportedException.java deleted file mode 100644 index ffa8a113..00000000 --- a/src/net/majorkernelpanic/streaming/exceptions/ConfNotSupportedException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.exceptions; - -public class ConfNotSupportedException extends RuntimeException { - - public ConfNotSupportedException(String message) { - super(message); - } - - private static final long serialVersionUID = 5876298277802827615L; -} diff --git a/src/net/majorkernelpanic/streaming/exceptions/InvalidSurfaceException.java b/src/net/majorkernelpanic/streaming/exceptions/InvalidSurfaceException.java deleted file mode 100644 index 45cd5b16..00000000 --- a/src/net/majorkernelpanic/streaming/exceptions/InvalidSurfaceException.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.exceptions; - -public class InvalidSurfaceException extends RuntimeException { - - private static final long serialVersionUID = -7238661340093544496L; - - public InvalidSurfaceException(String message) { - super(message); - } - -} diff --git a/src/net/majorkernelpanic/streaming/exceptions/StorageUnavailableException.java b/src/net/majorkernelpanic/streaming/exceptions/StorageUnavailableException.java deleted file mode 100644 index 05742d41..00000000 --- a/src/net/majorkernelpanic/streaming/exceptions/StorageUnavailableException.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.exceptions; - -import java.io.IOException; - -public class StorageUnavailableException extends IOException { - - public StorageUnavailableException(String message) { - super(message); - } - - private static final long serialVersionUID = -7537890350373995089L; -} diff --git a/src/net/majorkernelpanic/streaming/gl/SurfaceManager.java b/src/net/majorkernelpanic/streaming/gl/SurfaceManager.java deleted file mode 100644 index 02bf54ac..00000000 --- a/src/net/majorkernelpanic/streaming/gl/SurfaceManager.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -/* - * Based on the work of fadden - * - * Copyright 2012 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.majorkernelpanic.streaming.gl; - -import android.annotation.SuppressLint; -import android.opengl.EGL14; -import android.opengl.EGLConfig; -import android.opengl.EGLContext; -import android.opengl.EGLDisplay; -import android.opengl.EGLExt; -import android.opengl.EGLSurface; -import android.opengl.GLES20; -import android.view.Surface; - -@SuppressLint("NewApi") -public class SurfaceManager { - - public final static String TAG = "TextureManager"; - - private static final int EGL_RECORDABLE_ANDROID = 0x3142; - - private EGLContext mEGLContext = null; - private EGLContext mEGLSharedContext = null; - private EGLSurface mEGLSurface = null; - private EGLDisplay mEGLDisplay = null; - - private Surface mSurface; - - /** - * Creates an EGL context and an EGL surface. - */ - public SurfaceManager(Surface surface, SurfaceManager manager) { - mSurface = surface; - mEGLSharedContext = manager.mEGLContext; - eglSetup(); - } - - /** - * Creates an EGL context and an EGL surface. - */ - public SurfaceManager(Surface surface) { - mSurface = surface; - eglSetup(); - } - - public void makeCurrent() { - if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) - throw new RuntimeException("eglMakeCurrent failed"); - } - - public void swapBuffer() { - EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface); - } - - /** - * Sends the presentation time stamp to EGL. Time is expressed in nanoseconds. - */ - public void setPresentationTime(long nsecs) { - EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs); - checkEglError("eglPresentationTimeANDROID"); - } - - /** - * Prepares EGL. We want a GLES 2.0 context and a surface that supports recording. - */ - private void eglSetup() { - mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); - if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { - throw new RuntimeException("unable to get EGL14 display"); - } - int[] version = new int[2]; - if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { - throw new RuntimeException("unable to initialize EGL14"); - } - - // Configure EGL for recording and OpenGL ES 2.0. - int[] attribList; - if (mEGLSharedContext == null) { - attribList = new int[] { - EGL14.EGL_RED_SIZE, 8, - EGL14.EGL_GREEN_SIZE, 8, - EGL14.EGL_BLUE_SIZE, 8, - EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, - EGL14.EGL_NONE - }; - } else { - attribList = new int[] { - EGL14.EGL_RED_SIZE, 8, - EGL14.EGL_GREEN_SIZE, 8, - EGL14.EGL_BLUE_SIZE, 8, - EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, - EGL_RECORDABLE_ANDROID, 1, - EGL14.EGL_NONE - }; - } - EGLConfig[] configs = new EGLConfig[1]; - int[] numConfigs = new int[1]; - EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, - numConfigs, 0); - checkEglError("eglCreateContext RGB888+recordable ES2"); - - // Configure context for OpenGL ES 2.0. - int[] attrib_list = { - EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, - EGL14.EGL_NONE - }; - - if (mEGLSharedContext == null) { - mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, attrib_list, 0); - } else { - mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], mEGLSharedContext, attrib_list, 0); - } - checkEglError("eglCreateContext"); - - // Create a window surface, and attach it to the Surface we received. - int[] surfaceAttribs = { - EGL14.EGL_NONE - }; - mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], mSurface, - surfaceAttribs, 0); - checkEglError("eglCreateWindowSurface"); - - GLES20.glDisable(GLES20.GL_DEPTH_TEST); - GLES20.glDisable(GLES20.GL_CULL_FACE); - - } - - /** - * Discards all resources held by this class, notably the EGL context. Also releases the - * Surface that was passed to our constructor. - */ - public void release() { - if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { - EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, - EGL14.EGL_NO_CONTEXT); - EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface); - EGL14.eglDestroyContext(mEGLDisplay, mEGLContext); - EGL14.eglReleaseThread(); - EGL14.eglTerminate(mEGLDisplay); - } - mEGLDisplay = EGL14.EGL_NO_DISPLAY; - mEGLContext = EGL14.EGL_NO_CONTEXT; - mEGLSurface = EGL14.EGL_NO_SURFACE; - mSurface.release(); - } - - /** - * Checks for EGL errors. Throws an exception if one is found. - */ - private void checkEglError(String msg) { - int error; - if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) { - throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error)); - } - } - - - - -} diff --git a/src/net/majorkernelpanic/streaming/gl/SurfaceView.java b/src/net/majorkernelpanic/streaming/gl/SurfaceView.java deleted file mode 100644 index d78b1721..00000000 --- a/src/net/majorkernelpanic/streaming/gl/SurfaceView.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.gl; - -import java.util.concurrent.Semaphore; - -import net.majorkernelpanic.streaming.MediaStream; -import net.majorkernelpanic.streaming.video.VideoStream; -import android.content.Context; -import android.graphics.SurfaceTexture; -import android.graphics.SurfaceTexture.OnFrameAvailableListener; -import android.os.Handler; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Surface; -import android.view.SurfaceHolder; - -/** - * An enhanced SurfaceView in which the camera preview will be rendered. - * This class was needed for two reasons.
- * - * First, it allows to use to feed MediaCodec with the camera preview - * using the surface-to-buffer method while rendering it in a surface - * visible to the user. To force the surface-to-buffer method in - * libstreaming, call {@link MediaStream#setStreamingMethod(byte)} - * with {@link MediaStream#MODE_MEDIACODEC_API_2}.
- * - * Second, it allows to force the aspect ratio of the SurfaceView - * to match the aspect ratio of the camera preview, so that the - * preview do not appear distorted to the user of your app. To do - * that, call {@link SurfaceView#setAspectRatioMode(int)} with - * {@link SurfaceView#ASPECT_RATIO_PREVIEW} after creating your - * {@link SurfaceView}.
- * - */ -public class SurfaceView extends android.view.SurfaceView implements Runnable, OnFrameAvailableListener, SurfaceHolder.Callback { - - public final static String TAG = "SurfaceView"; - - /** - * The aspect ratio of the surface view will be equal - * to the aspect ration of the camera preview. - **/ - public static final int ASPECT_RATIO_PREVIEW = 0x01; - - /** The surface view will fill completely fill its parent. */ - public static final int ASPECT_RATIO_STRETCH = 0x00; - - private Thread mThread = null; - private Handler mHandler = null; - private boolean mFrameAvailable = false; - private boolean mRunning = true; - private int mAspectRatioMode = ASPECT_RATIO_STRETCH; - - // The surface in which the preview is rendered - private SurfaceManager mViewSurfaceManager = null; - - // The input surface of the MediaCodec - private SurfaceManager mCodecSurfaceManager = null; - - // Handles the rendering of the SurfaceTexture we got - // from the camera, onto a Surface - private TextureManager mTextureManager = null; - - private final Semaphore mLock = new Semaphore(0); - private final Object mSyncObject = new Object(); - - // Allows to force the aspect ratio of the preview - private ViewAspectRatioMeasurer mVARM = new ViewAspectRatioMeasurer(); - - public SurfaceView(Context context, AttributeSet attrs) { - super(context, attrs); - mHandler = new Handler(); - getHolder().addCallback(this); - } - - public void setAspectRatioMode(int mode) { - mAspectRatioMode = mode; - } - - public SurfaceTexture getSurfaceTexture() { - return mTextureManager.getSurfaceTexture(); - } - - public void addMediaCodecSurface(Surface surface) { - synchronized (mSyncObject) { - mCodecSurfaceManager = new SurfaceManager(surface,mViewSurfaceManager); - } - } - - public void removeMediaCodecSurface() { - synchronized (mSyncObject) { - if (mCodecSurfaceManager != null) { - mCodecSurfaceManager.release(); - mCodecSurfaceManager = null; - } - } - } - - public void startGLThread() { - Log.d(TAG,"Thread started."); - if (mTextureManager == null) { - mTextureManager = new TextureManager(); - } - if (mTextureManager.getSurfaceTexture() == null) { - mThread = new Thread(SurfaceView.this); - mRunning = true; - mThread.start(); - mLock.acquireUninterruptibly(); - } - } - - @Override - public void run() { - - mViewSurfaceManager = new SurfaceManager(getHolder().getSurface()); - mViewSurfaceManager.makeCurrent(); - mTextureManager.createTexture().setOnFrameAvailableListener(this); - - mLock.release(); - - try { - long ts = 0, oldts = 0; - while (mRunning) { - synchronized (mSyncObject) { - mSyncObject.wait(2500); - if (mFrameAvailable) { - mFrameAvailable = false; - - mViewSurfaceManager.makeCurrent(); - mTextureManager.updateFrame(); - mTextureManager.drawFrame(); - mViewSurfaceManager.swapBuffer(); - - if (mCodecSurfaceManager != null) { - mCodecSurfaceManager.makeCurrent(); - mTextureManager.drawFrame(); - oldts = ts; - ts = mTextureManager.getSurfaceTexture().getTimestamp(); - //Log.d(TAG,"FPS: "+(1000000000/(ts-oldts))); - mCodecSurfaceManager.setPresentationTime(ts); - mCodecSurfaceManager.swapBuffer(); - } - - } else { - Log.e(TAG,"No frame received !"); - } - } - } - } catch (InterruptedException ignore) { - } finally { - mViewSurfaceManager.release(); - mTextureManager.release(); - } - } - - @Override - public void onFrameAvailable(SurfaceTexture surfaceTexture) { - synchronized (mSyncObject) { - mFrameAvailable = true; - mSyncObject.notifyAll(); - } - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, - int height) { - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - if (mThread != null) { - mThread.interrupt(); - } - mRunning = false; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (mVARM.getAspectRatio() > 0 && mAspectRatioMode == ASPECT_RATIO_PREVIEW) { - mVARM.measure(widthMeasureSpec, heightMeasureSpec); - setMeasuredDimension(mVARM.getMeasuredWidth(), mVARM.getMeasuredHeight()); - } else { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - } - - /** - * Requests a certain aspect ratio for the preview. You don't have to call this yourself, - * the {@link VideoStream} will do it when it's needed. - */ - public void requestAspectRatio(double aspectRatio) { - if (mVARM.getAspectRatio() != aspectRatio) { - mVARM.setAspectRatio(aspectRatio); - mHandler.post(new Runnable() { - @Override - public void run() { - if (mAspectRatioMode == ASPECT_RATIO_PREVIEW) { - requestLayout(); - } - } - }); - } - } - - /** - * This class is a helper to measure views that require a specific aspect ratio. - * @author Jesper Borgstrup - */ - public class ViewAspectRatioMeasurer { - - private double aspectRatio; - - public void setAspectRatio(double aspectRatio) { - this.aspectRatio = aspectRatio; - } - - public double getAspectRatio() { - return this.aspectRatio; - } - - /** - * Measure with the aspect ratio given at construction.
- *
- * After measuring, get the width and height with the {@link #getMeasuredWidth()} - * and {@link #getMeasuredHeight()} methods, respectively. - * @param widthMeasureSpec The width MeasureSpec passed in your View.onMeasure() method - * @param heightMeasureSpec The height MeasureSpec passed in your View.onMeasure() method - */ - public void measure(int widthMeasureSpec, int heightMeasureSpec) { - measure(widthMeasureSpec, heightMeasureSpec, this.aspectRatio); - } - - /** - * Measure with a specific aspect ratio
- *
- * After measuring, get the width and height with the {@link #getMeasuredWidth()} - * and {@link #getMeasuredHeight()} methods, respectively. - * @param widthMeasureSpec The width MeasureSpec passed in your View.onMeasure() method - * @param heightMeasureSpec The height MeasureSpec passed in your View.onMeasure() method - * @param aspectRatio The aspect ratio to calculate measurements in respect to - */ - public void measure(int widthMeasureSpec, int heightMeasureSpec, double aspectRatio) { - int widthMode = MeasureSpec.getMode( widthMeasureSpec ); - int widthSize = widthMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize( widthMeasureSpec ); - int heightMode = MeasureSpec.getMode( heightMeasureSpec ); - int heightSize = heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize( heightMeasureSpec ); - - if ( heightMode == MeasureSpec.EXACTLY && widthMode == MeasureSpec.EXACTLY ) { - /* - * Possibility 1: Both width and height fixed - */ - measuredWidth = widthSize; - measuredHeight = heightSize; - - } else if ( heightMode == MeasureSpec.EXACTLY ) { - /* - * Possibility 2: Width dynamic, height fixed - */ - measuredWidth = (int) Math.min( widthSize, heightSize * aspectRatio ); - measuredHeight = (int) (measuredWidth / aspectRatio); - - } else if ( widthMode == MeasureSpec.EXACTLY ) { - /* - * Possibility 3: Width fixed, height dynamic - */ - measuredHeight = (int) Math.min( heightSize, widthSize / aspectRatio ); - measuredWidth = (int) (measuredHeight * aspectRatio); - - } else { - /* - * Possibility 4: Both width and height dynamic - */ - if ( widthSize > heightSize * aspectRatio ) { - measuredHeight = heightSize; - measuredWidth = (int)( measuredHeight * aspectRatio ); - } else { - measuredWidth = widthSize; - measuredHeight = (int) (measuredWidth / aspectRatio); - } - - } - } - - private Integer measuredWidth = null; - /** - * Get the width measured in the latest call to measure(). - */ - public int getMeasuredWidth() { - if ( measuredWidth == null ) { - throw new IllegalStateException( "You need to run measure() before trying to get measured dimensions" ); - } - return measuredWidth; - } - - private Integer measuredHeight = null; - /** - * Get the height measured in the latest call to measure(). - */ - public int getMeasuredHeight() { - if ( measuredHeight == null ) { - throw new IllegalStateException( "You need to run measure() before trying to get measured dimensions" ); - } - return measuredHeight; - } - - } - -} diff --git a/src/net/majorkernelpanic/streaming/gl/TextureManager.java b/src/net/majorkernelpanic/streaming/gl/TextureManager.java deleted file mode 100644 index 3e637e2c..00000000 --- a/src/net/majorkernelpanic/streaming/gl/TextureManager.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -/* - * Based on the work of fadden - * - * Copyright 2012 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.majorkernelpanic.streaming.gl; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; - -import android.annotation.SuppressLint; -import android.graphics.SurfaceTexture; -import android.opengl.GLES11Ext; -import android.opengl.GLES20; -import android.opengl.Matrix; -import android.util.Log; - -/** - * Code for rendering a texture onto a surface using OpenGL ES 2.0. - */ -@SuppressLint("InlinedApi") -public class TextureManager { - - public final static String TAG = "TextureManager"; - - private static final int FLOAT_SIZE_BYTES = 4; - private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES; - private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0; - private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3; - private final float[] mTriangleVerticesData = { - // X, Y, Z, U, V - -1.0f, -1.0f, 0, 0.f, 0.f, - 1.0f, -1.0f, 0, 1.f, 0.f, - -1.0f, 1.0f, 0, 0.f, 1.f, - 1.0f, 1.0f, 0, 1.f, 1.f, - }; - - private FloatBuffer mTriangleVertices; - - private static final String VERTEX_SHADER = - "uniform mat4 uMVPMatrix;\n" + - "uniform mat4 uSTMatrix;\n" + - "attribute vec4 aPosition;\n" + - "attribute vec4 aTextureCoord;\n" + - "varying vec2 vTextureCoord;\n" + - "void main() {\n" + - " gl_Position = uMVPMatrix * aPosition;\n" + - " vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" + - "}\n"; - - private static final String FRAGMENT_SHADER = - "#extension GL_OES_EGL_image_external : require\n" + - "precision mediump float;\n" + // highp here doesn't seem to matter - "varying vec2 vTextureCoord;\n" + - "uniform samplerExternalOES sTexture;\n" + - "void main() {\n" + - " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + - "}\n"; - - private float[] mMVPMatrix = new float[16]; - private float[] mSTMatrix = new float[16]; - - private int mProgram; - private int mTextureID = -12345; - private int muMVPMatrixHandle; - private int muSTMatrixHandle; - private int maPositionHandle; - private int maTextureHandle; - - private SurfaceTexture mSurfaceTexture; - - public TextureManager() { - mTriangleVertices = ByteBuffer.allocateDirect( - mTriangleVerticesData.length * FLOAT_SIZE_BYTES) - .order(ByteOrder.nativeOrder()).asFloatBuffer(); - mTriangleVertices.put(mTriangleVerticesData).position(0); - - Matrix.setIdentityM(mSTMatrix, 0); - } - - public int getTextureId() { - return mTextureID; - } - - public SurfaceTexture getSurfaceTexture() { - return mSurfaceTexture; - } - - public void updateFrame() { - mSurfaceTexture.updateTexImage(); - } - - public void drawFrame() { - checkGlError("onDrawFrame start"); - mSurfaceTexture.getTransformMatrix(mSTMatrix); - - //GLES20.glClearColor(0.0f, 1.0f, 0.0f, 1.0f); - //GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT); - - GLES20.glUseProgram(mProgram); - checkGlError("glUseProgram"); - - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0); - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); - - mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); - GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, - TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); - checkGlError("glVertexAttribPointer maPosition"); - GLES20.glEnableVertexAttribArray(maPositionHandle); - checkGlError("glEnableVertexAttribArray maPositionHandle"); - - mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET); - GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, - TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); - checkGlError("glVertexAttribPointer maTextureHandle"); - GLES20.glEnableVertexAttribArray(maTextureHandle); - checkGlError("glEnableVertexAttribArray maTextureHandle"); - - Matrix.setIdentityM(mMVPMatrix, 0); - GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0); - GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0); - - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); - checkGlError("glDrawArrays"); - GLES20.glFinish(); - } - - /** - * Initializes GL state. Call this after the EGL surface has been created and made current. - */ - public SurfaceTexture createTexture() { - mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER); - if (mProgram == 0) { - throw new RuntimeException("failed creating program"); - } - maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition"); - checkGlError("glGetAttribLocation aPosition"); - if (maPositionHandle == -1) { - throw new RuntimeException("Could not get attrib location for aPosition"); - } - maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord"); - checkGlError("glGetAttribLocation aTextureCoord"); - if (maTextureHandle == -1) { - throw new RuntimeException("Could not get attrib location for aTextureCoord"); - } - - muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); - checkGlError("glGetUniformLocation uMVPMatrix"); - if (muMVPMatrixHandle == -1) { - throw new RuntimeException("Could not get attrib location for uMVPMatrix"); - } - - muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix"); - checkGlError("glGetUniformLocation uSTMatrix"); - if (muSTMatrixHandle == -1) { - throw new RuntimeException("Could not get attrib location for uSTMatrix"); - } - - int[] textures = new int[1]; - GLES20.glGenTextures(1, textures, 0); - - mTextureID = textures[0]; - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); - checkGlError("glBindTexture mTextureID"); - - GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, - GLES20.GL_NEAREST); - GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, - GLES20.GL_LINEAR); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, - GLES20.GL_CLAMP_TO_EDGE); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, - GLES20.GL_CLAMP_TO_EDGE); - checkGlError("glTexParameter"); - - mSurfaceTexture = new SurfaceTexture(mTextureID); - return mSurfaceTexture; - } - - public void release() { - mSurfaceTexture = null; - } - - /** - * Replaces the fragment shader. Pass in null to reset to default. - */ - public void changeFragmentShader(String fragmentShader) { - if (fragmentShader == null) { - fragmentShader = FRAGMENT_SHADER; - } - GLES20.glDeleteProgram(mProgram); - mProgram = createProgram(VERTEX_SHADER, fragmentShader); - if (mProgram == 0) { - throw new RuntimeException("failed creating program"); - } - } - - private int loadShader(int shaderType, String source) { - int shader = GLES20.glCreateShader(shaderType); - checkGlError("glCreateShader type=" + shaderType); - GLES20.glShaderSource(shader, source); - GLES20.glCompileShader(shader); - int[] compiled = new int[1]; - GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); - if (compiled[0] == 0) { - Log.e(TAG, "Could not compile shader " + shaderType + ":"); - Log.e(TAG, " " + GLES20.glGetShaderInfoLog(shader)); - GLES20.glDeleteShader(shader); - shader = 0; - } - return shader; - } - - private int createProgram(String vertexSource, String fragmentSource) { - int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); - if (vertexShader == 0) { - return 0; - } - int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); - if (pixelShader == 0) { - return 0; - } - - int program = GLES20.glCreateProgram(); - checkGlError("glCreateProgram"); - if (program == 0) { - Log.e(TAG, "Could not create program"); - } - GLES20.glAttachShader(program, vertexShader); - checkGlError("glAttachShader"); - GLES20.glAttachShader(program, pixelShader); - checkGlError("glAttachShader"); - GLES20.glLinkProgram(program); - int[] linkStatus = new int[1]; - GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); - if (linkStatus[0] != GLES20.GL_TRUE) { - Log.e(TAG, "Could not link program: "); - Log.e(TAG, GLES20.glGetProgramInfoLog(program)); - GLES20.glDeleteProgram(program); - program = 0; - } - return program; - } - - public void checkGlError(String op) { - int error; - while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { - Log.e(TAG, op + ": glError " + error); - throw new RuntimeException(op + ": glError " + error); - } - } -} diff --git a/src/net/majorkernelpanic/streaming/hw/CodecManager.java b/src/net/majorkernelpanic/streaming/hw/CodecManager.java deleted file mode 100644 index 8353b26e..00000000 --- a/src/net/majorkernelpanic/streaming/hw/CodecManager.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.hw; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; - -import android.annotation.SuppressLint; -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.util.Log; - -@SuppressLint("InlinedApi") -public class CodecManager { - - public final static String TAG = "CodecManager"; - - public static final int[] SUPPORTED_COLOR_FORMATS = { - MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar, - MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar, - MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar, - MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar, - MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar - }; - - private static Codec[] sEncoders = null; - private static Codec[] sDecoders = null; - - static class Codec { - public Codec(String name, Integer[] formats) { - this.name = name; - this.formats = formats; - } - public String name; - public Integer[] formats; - } - - /** - * Lists all encoders that claim to support a color format that we know how to use. - * @return A list of those encoders - */ - @SuppressLint("NewApi") - public synchronized static Codec[] findEncodersForMimeType(String mimeType) { - if (sEncoders != null) return sEncoders; - - ArrayList encoders = new ArrayList(); - - // We loop through the encoders, apparently this can take up to a sec (testes on a GS3) - for(int j = MediaCodecList.getCodecCount() - 1; j >= 0; j--){ - MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(j); - if (!codecInfo.isEncoder()) continue; - - String[] types = codecInfo.getSupportedTypes(); - for (int i = 0; i < types.length; i++) { - if (types[i].equalsIgnoreCase(mimeType)) { - try { - MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType); - Set formats = new HashSet(); - - // And through the color formats supported - for (int k = 0; k < capabilities.colorFormats.length; k++) { - int format = capabilities.colorFormats[k]; - - for (int l=0;l decoders = new ArrayList(); - - // We loop through the decoders, apparently this can take up to a sec (testes on a GS3) - for(int j = MediaCodecList.getCodecCount() - 1; j >= 0; j--){ - MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(j); - if (codecInfo.isEncoder()) continue; - - String[] types = codecInfo.getSupportedTypes(); - for (int i = 0; i < types.length; i++) { - if (types[i].equalsIgnoreCase(mimeType)) { - try { - MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType); - Set formats = new HashSet(); - - // And through the color formats supported - for (int k = 0; k < capabilities.colorFormats.length; k++) { - int format = capabilities.colorFormats[k]; - - for (int l=0;l - * Feeding the encoder with a surface is not tested here. - * Some bugs you may have encountered:
- *
    - *
  • U and V panes reversed
  • - *
  • Some padding is needed after the Y pane
  • - *
  • stride!=width or slice-height!=height
  • - *
- */ -@SuppressLint("NewApi") -public class EncoderDebugger { - - public final static String TAG = "EncoderDebugger"; - - /** Prefix that will be used for all shared preferences saved by libstreaming. */ - private static final String PREF_PREFIX = "libstreaming-"; - - /** - * If this is set to false the test will be run only once and the result - * will be saved in the shared preferences. - */ - private static final boolean DEBUG = false; - - /** Set this to true to see more logs. */ - private static final boolean VERBOSE = false; - - /** Will be incremented every time this test is modified. */ - private static final int VERSION = 3; - - /** Bitrate that will be used with the encoder. */ - private final static int BITRATE = 1000000; - - /** Framerate that will be used to test the encoder. */ - private final static int FRAMERATE = 20; - - private final static String MIME_TYPE = "video/avc"; - - private final static int NB_DECODED = 34; - private final static int NB_ENCODED = 50; - - private int mDecoderColorFormat, mEncoderColorFormat; - private String mDecoderName, mEncoderName, mErrorLog; - private MediaCodec mEncoder, mDecoder; - private int mWidth, mHeight, mSize; - private byte[] mSPS, mPPS; - private byte[] mData, mInitialImage; - private MediaFormat mDecOutputFormat; - private NV21Convertor mNV21; - private SharedPreferences mPreferences; - private byte[][] mVideo, mDecodedVideo; - private String mB64PPS, mB64SPS; - - public synchronized static void asyncDebug(final Context context, final int width, final int height) { - new Thread(new Runnable() { - @Override - public void run() { - try { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - debug(prefs, width, height); - } catch (Exception e) {} - } - }).start(); - } - - public synchronized static EncoderDebugger debug(Context context, int width, int height) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - return debug(prefs, width, height); - } - - public synchronized static EncoderDebugger debug(SharedPreferences prefs, int width, int height) { - EncoderDebugger debugger = new EncoderDebugger(prefs, width, height); - debugger.debug(); - return debugger; - } - - public String getB64PPS() { - return mB64PPS; - } - - public String getB64SPS() { - return mB64SPS; - } - - public String getEncoderName() { - return mEncoderName; - } - - public int getEncoderColorFormat() { - return mEncoderColorFormat; - } - - /** This {@link NV21Convertor} will do the necessary work to feed properly the encoder. */ - public NV21Convertor getNV21Convertor() { - return mNV21; - } - - /** A log of all the errors that occured during the test. */ - public String getErrorLog() { - return mErrorLog; - } - - private EncoderDebugger(SharedPreferences prefs, int width, int height) { - mPreferences = prefs; - mWidth = width; - mHeight = height; - mSize = width*height; - reset(); - } - - private void reset() { - mNV21 = new NV21Convertor(); - mVideo = new byte[NB_ENCODED][]; - mDecodedVideo = new byte[NB_DECODED][]; - mErrorLog = ""; - mPPS = null; - mSPS = null; - } - - private void debug() { - - // If testing the phone again is not needed, - // we just restore the result from the shared preferences - if (!checkTestNeeded()) { - String resolution = mWidth+"x"+mHeight+"-"; - - boolean success = mPreferences.getBoolean(PREF_PREFIX+resolution+"success",false); - if (!success) { - throw new RuntimeException("Phone not supported with this resolution ("+mWidth+"x"+mHeight+")"); - } - - mNV21.setSize(mWidth, mHeight); - mNV21.setSliceHeigth(mPreferences.getInt(PREF_PREFIX+resolution+"sliceHeight", 0)); - mNV21.setStride(mPreferences.getInt(PREF_PREFIX+resolution+"stride", 0)); - mNV21.setYPadding(mPreferences.getInt(PREF_PREFIX+resolution+"padding", 0)); - mNV21.setPlanar(mPreferences.getBoolean(PREF_PREFIX+resolution+"planar", false)); - mNV21.setColorPanesReversed(mPreferences.getBoolean(PREF_PREFIX+resolution+"reversed", false)); - mEncoderName = mPreferences.getString(PREF_PREFIX+resolution+"encoderName", ""); - mEncoderColorFormat = mPreferences.getInt(PREF_PREFIX+resolution+"colorFormat", 0); - mB64PPS = mPreferences.getString(PREF_PREFIX+resolution+"pps", ""); - mB64SPS = mPreferences.getString(PREF_PREFIX+resolution+"sps", ""); - - return; - } - - if (VERBOSE) Log.d(TAG, ">>>> Testing the phone for resolution "+mWidth+"x"+mHeight); - - // Builds a list of available encoders and decoders we may be able to use - // because they support some nice color formats - Codec[] encoders = CodecManager.findEncodersForMimeType(MIME_TYPE); - Codec[] decoders = CodecManager.findDecodersForMimeType(MIME_TYPE); - - int count = 0, n = 1; - for (int i=0;i> Test "+(n++)+"/"+count+": "+mEncoderName+" with color format "+mEncoderColorFormat+" at "+mWidth+"x"+mHeight); - - // Converts from NV21 to YUV420 with the specified parameters - mNV21.setSize(mWidth, mHeight); - mNV21.setSliceHeigth(mHeight); - mNV21.setStride(mWidth); - mNV21.setYPadding(0); - mNV21.setEncoderColorFormat(mEncoderColorFormat); - - // /!\ NV21Convertor can directly modify the input - createTestImage(); - mData = mNV21.convert(mInitialImage); - - try { - - // Starts the encoder - configureEncoder(); - searchSPSandPPS(); - - if (VERBOSE) Log.v(TAG, "SPS and PPS in b64: SPS="+mB64SPS+", PPS="+mB64PPS); - - // Feeds the encoder with an image repeatidly to produce some NAL units - encode(); - - // We now try to decode the NALs with decoders available on the phone - boolean decoded = false; - for (int k=0;k0) { - if (padding<4096) { - if (VERBOSE) Log.d(TAG, "Some padding is needed: "+padding); - mNV21.setYPadding(padding); - createTestImage(); - mData = mNV21.convert(mInitialImage); - encodeDecode(); - } else { - // TODO: try again with a different sliceHeight - // TODO: try again with the "slice-height" param - throw new RuntimeException("It is likely that sliceHeight!=height"); - } - } - - createTestImage(); - if (!compareChromaPanes(false)) { - if (compareChromaPanes(true)) { - mNV21.setColorPanesReversed(true); - if (VERBOSE) Log.d(TAG, "U and V pane are reversed"); - } else { - throw new RuntimeException("Incorrect U or V pane..."); - } - } - - saveTestResult(true); - Log.v(TAG, "The encoder "+mEncoderName+" is usable with resolution "+mWidth+"x"+mHeight); - return; - - } catch (Exception e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); - String stack = sw.toString(); - String str = "Encoder "+mEncoderName+" cannot be used with color format "+mEncoderColorFormat; - if (VERBOSE) Log.e(TAG, str, e); - mErrorLog += str + "\n" + stack; - e.printStackTrace(); - } finally { - releaseEncoder(); - } - - } - } - - saveTestResult(false); - Log.e(TAG,"No usable encoder were found on the phone for resolution "+mWidth+"x"+mHeight); - throw new RuntimeException("No usable encoder were found on the phone for resolution "+mWidth+"x"+mHeight); - - } - - private boolean checkTestNeeded() { - String resolution = mWidth+"x"+mHeight+"-"; - - // Forces the test - if (DEBUG || mPreferences==null) return true; - - // If the sdk has changed on the phone, or the version of the test - // it has to be run again - if (mPreferences.contains(PREF_PREFIX+resolution+"lastSdk")) { - int lastSdk = mPreferences.getInt(PREF_PREFIX+resolution+"lastSdk", 0); - int lastVersion = mPreferences.getInt(PREF_PREFIX+resolution+"lastVersion", 0); - if (Build.VERSION.SDK_INT>lastSdk || VERSION>lastVersion) { - return true; - } - } else { - return true; - } - return false; - } - - - /** - * Saves the result of the test in the shared preferences, - * we will run it again only if the SDK has changed on the phone, - * or if this test has been modified. - */ - private void saveTestResult(boolean success) { - String resolution = mWidth+"x"+mHeight+"-"; - Editor editor = mPreferences.edit(); - - editor.putBoolean(PREF_PREFIX+resolution+"success", success); - - if (success) { - editor.putInt(PREF_PREFIX+resolution+"lastSdk", Build.VERSION.SDK_INT); - editor.putInt(PREF_PREFIX+resolution+"lastVersion", VERSION); - editor.putInt(PREF_PREFIX+resolution+"sliceHeight", mNV21.getSliceHeigth()); - editor.putInt(PREF_PREFIX+resolution+"stride", mNV21.getStride()); - editor.putInt(PREF_PREFIX+resolution+"padding", mNV21.getYPadding()); - editor.putBoolean(PREF_PREFIX+resolution+"planar", mNV21.getPlanar()); - editor.putBoolean(PREF_PREFIX+resolution+"reversed", mNV21.getUVPanesReversed()); - editor.putString(PREF_PREFIX+resolution+"encoderName", mEncoderName); - editor.putInt(PREF_PREFIX+resolution+"colorFormat", mEncoderColorFormat); - editor.putString(PREF_PREFIX+resolution+"encoderName", mEncoderName); - editor.putString(PREF_PREFIX+resolution+"pps", mB64PPS); - editor.putString(PREF_PREFIX+resolution+"sps", mB64SPS); - } - - editor.commit(); - } - - /** - * Creates the test image that will be used to feed the encoder. - */ - private void createTestImage() { - mInitialImage = new byte[3*mSize/2]; - for (int i=0;i50 && e>50) { - mDecodedVideo[j] = null; - f++; - break; - } - } - } - return f<=NB_DECODED/2; - } - - private int checkPaddingNeeded() { - int i = 0, j = 3*mSize/2-1, max = 0; - int[] r = new int[NB_DECODED]; - for (int k=0;k0) { - r[k] = ((i>>6)<<6); - max = r[k]>max ? r[k] : max; - if (VERBOSE) Log.e(TAG,"Padding needed: "+r[k]); - } else { - if (VERBOSE) Log.v(TAG,"No padding needed."); - } - } - } - - return ((max>>6)<<6); - } - - /** - * Compares the U or V pane of the initial image, and the U or V pane - * after having encoded & decoded the image. - */ - private boolean compareChromaPanes(boolean crossed) { - int d, f = 0; - - for (int j=0;j50) { - //if (VERBOSE) Log.e(TAG,"BUG "+(i-mSize)+" d "+d); - f++; - break; - } - } - - // We compare the V pane before with the U pane after - } else { - for (int i=mSize;i<3*mSize/2;i+=2) { - d = (mInitialImage[i]&0xFF) - (mDecodedVideo[j][i+1]&0xFF); - d = d<0 ? -d : d; - if (d>50) { - f++; - } - } - } - } - } - return f<=NB_DECODED/2; - } - - /** - * Converts the image obtained from the decoder to NV21. - */ - private void convertToNV21(int k) { - byte[] buffer = new byte[3*mSize/2]; - - int stride = mWidth, sliceHeight = mHeight; - int colorFormat = mDecoderColorFormat; - boolean planar = false; - - if (mDecOutputFormat != null) { - MediaFormat format = mDecOutputFormat; - if (format != null) { - if (format.containsKey("slice-height")) { - sliceHeight = format.getInteger("slice-height"); - if (sliceHeight0) { - colorFormat = format.getInteger(MediaFormat.KEY_COLOR_FORMAT); - } - } - } - } - - switch (colorFormat) { - case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: - case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar: - case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar: - planar = false; - break; - case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: - case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar: - planar = true; - break; - } - - for (int i=0;i=0) { - decInputBuffers[decInputIndex].clear(); - decInputBuffers[decInputIndex].put(prefix); - decInputBuffers[decInputIndex].put(mSPS); - mDecoder.queueInputBuffer(decInputIndex, 0, decInputBuffers[decInputIndex].position(), timestamp(), 0); - } else { - if (VERBOSE) Log.e(TAG,"No buffer available !"); - } - - decInputIndex = mDecoder.dequeueInputBuffer(1000000/FRAMERATE); - if (decInputIndex>=0) { - decInputBuffers[decInputIndex].clear(); - decInputBuffers[decInputIndex].put(prefix); - decInputBuffers[decInputIndex].put(mPPS); - mDecoder.queueInputBuffer(decInputIndex, 0, decInputBuffers[decInputIndex].position(), timestamp(), 0); - } else { - if (VERBOSE) Log.e(TAG,"No buffer available !"); - } - - - } - - private void releaseDecoder() { - if (mDecoder != null) { - try { - mDecoder.stop(); - } catch (Exception ignore) {} - try { - mDecoder.release(); - } catch (Exception ignore) {} - } - } - - /** - * Tries to obtain the SPS and the PPS for the encoder. - */ - private long searchSPSandPPS() { - - ByteBuffer[] inputBuffers = mEncoder.getInputBuffers(); - ByteBuffer[] outputBuffers = mEncoder.getOutputBuffers(); - BufferInfo info = new BufferInfo(); - byte[] csd = new byte[128]; - int len = 0, p = 4, q = 4; - long elapsed = 0, now = timestamp(); - - while (elapsed<3000000 && (mSPS==null || mPPS==null)) { - - // Some encoders won't give us the SPS and PPS unless they receive something to encode first... - int bufferIndex = mEncoder.dequeueInputBuffer(1000000/FRAMERATE); - if (bufferIndex>=0) { - check(inputBuffers[bufferIndex].capacity()>=mData.length, "The input buffer is not big enough."); - inputBuffers[bufferIndex].clear(); - inputBuffers[bufferIndex].put(mData, 0, mData.length); - mEncoder.queueInputBuffer(bufferIndex, 0, mData.length, timestamp(), 0); - } else { - if (VERBOSE) Log.e(TAG,"No buffer available !"); - } - - // We are looking for the SPS and the PPS here. As always, Android is very inconsistent, I have observed that some - // encoders will give those parameters through the MediaFormat object (that is the normal behaviour). - // But some other will not, in that case we try to find a NAL unit of type 7 or 8 in the byte stream outputed by the encoder... - - int index = mEncoder.dequeueOutputBuffer(info, 1000000/FRAMERATE); - - if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - - // The PPS and PPS shoud be there - MediaFormat format = mEncoder.getOutputFormat(); - ByteBuffer spsb = format.getByteBuffer("csd-0"); - ByteBuffer ppsb = format.getByteBuffer("csd-1"); - mSPS = new byte[spsb.capacity()-4]; - spsb.position(4); - spsb.get(mSPS,0,mSPS.length); - mPPS = new byte[ppsb.capacity()-4]; - ppsb.position(4); - ppsb.get(mPPS,0,mPPS.length); - break; - - } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { - outputBuffers = mEncoder.getOutputBuffers(); - } else if (index>=0) { - - len = info.size; - if (len<128) { - outputBuffers[index].get(csd,0,len); - if (len>0 && csd[0]==0 && csd[1]==0 && csd[2]==0 && csd[3]==1) { - // Parses the SPS and PPS, they could be in two different packets and in a different order - //depending on the phone so we don't make any assumption about that - while (p=len) p=len; - if ((csd[q]&0x1F)==7) { - mSPS = new byte[p-q]; - System.arraycopy(csd, q, mSPS, 0, p-q); - } else { - mPPS = new byte[p-q]; - System.arraycopy(csd, q, mPPS, 0, p-q); - } - p += 4; - q = p; - } - } - } - mEncoder.releaseOutputBuffer(index, false); - } - - elapsed = timestamp() - now; - } - - check(mPPS != null & mSPS != null, "Could not determine the SPS & PPS."); - mB64PPS = Base64.encodeToString(mPPS, 0, mPPS.length, Base64.NO_WRAP); - mB64SPS = Base64.encodeToString(mSPS, 0, mSPS.length, Base64.NO_WRAP); - - return elapsed; - } - - private long encode() { - int n = 0; - long elapsed = 0, now = timestamp(); - int encOutputIndex = 0, encInputIndex = 0; - BufferInfo info = new BufferInfo(); - ByteBuffer[] encInputBuffers = mEncoder.getInputBuffers(); - ByteBuffer[] encOutputBuffers = mEncoder.getOutputBuffers(); - - while (elapsed<5000000) { - // Feeds the encoder with an image - encInputIndex = mEncoder.dequeueInputBuffer(1000000/FRAMERATE); - if (encInputIndex>=0) { - check(encInputBuffers[encInputIndex].capacity()>=mData.length, "The input buffer is not big enough."); - encInputBuffers[encInputIndex].clear(); - encInputBuffers[encInputIndex].put(mData, 0, mData.length); - mEncoder.queueInputBuffer(encInputIndex, 0, mData.length, timestamp(), 0); - } else { - if (VERBOSE) Log.d(TAG,"No buffer available !"); - } - - // Tries to get a NAL unit - encOutputIndex = mEncoder.dequeueOutputBuffer(info, 1000000/FRAMERATE); - if (encOutputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { - encOutputBuffers = mEncoder.getOutputBuffers(); - } else if (encOutputIndex>=0) { - mVideo[n] = new byte[info.size]; - encOutputBuffers[encOutputIndex].clear(); - encOutputBuffers[encOutputIndex].get(mVideo[n++], 0, info.size); - mEncoder.releaseOutputBuffer(encOutputIndex, false); - if (n>=NB_ENCODED) { - flushMediaCodec(mEncoder); - return elapsed; - } - } - - elapsed = timestamp() - now; - } - - throw new RuntimeException("The encoder is too slow."); - - } - - /** - * @param withPrefix If set to true, the decoder will be fed with NALs preceeded with 0x00000001. - * @return How long it took to decode all the NALs - */ - private long decode(boolean withPrefix) { - int n = 0, i = 0, j = 0; - long elapsed = 0, now = timestamp(); - int decInputIndex = 0, decOutputIndex = 0; - ByteBuffer[] decInputBuffers = mDecoder.getInputBuffers(); - ByteBuffer[] decOutputBuffers = mDecoder.getOutputBuffers(); - BufferInfo info = new BufferInfo(); - - while (elapsed<3000000) { - - // Feeds the decoder with a NAL unit - if (i=0) { - int l1 = decInputBuffers[decInputIndex].capacity(); - int l2 = mVideo[i].length; - decInputBuffers[decInputIndex].clear(); - - if ((withPrefix && hasPrefix(mVideo[i])) || (!withPrefix && !hasPrefix(mVideo[i]))) { - check(l1>=l2, "The decoder input buffer is not big enough (nal="+l2+", capacity="+l1+")."); - decInputBuffers[decInputIndex].put(mVideo[i],0,mVideo[i].length); - } else if (withPrefix && !hasPrefix(mVideo[i])) { - check(l1>=l2+4, "The decoder input buffer is not big enough (nal="+(l2+4)+", capacity="+l1+")."); - decInputBuffers[decInputIndex].put(new byte[] {0,0,0,1}); - decInputBuffers[decInputIndex].put(mVideo[i],0,mVideo[i].length); - } else if (!withPrefix && hasPrefix(mVideo[i])) { - check(l1>=l2-4, "The decoder input buffer is not big enough (nal="+(l2-4)+", capacity="+l1+")."); - decInputBuffers[decInputIndex].put(mVideo[i],4,mVideo[i].length-4); - } - - mDecoder.queueInputBuffer(decInputIndex, 0, l2, timestamp(), 0); - i++; - } else { - if (VERBOSE) Log.d(TAG,"No buffer available !"); - } - } - - // Tries to get a decoded image - decOutputIndex = mDecoder.dequeueOutputBuffer(info, 1000000/FRAMERATE); - if (decOutputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { - decOutputBuffers = mDecoder.getOutputBuffers(); - } else if (decOutputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - mDecOutputFormat = mDecoder.getOutputFormat(); - } else if (decOutputIndex>=0) { - if (n>2) { - // We have successfully encoded and decoded an image ! - int length = info.size; - mDecodedVideo[j] = new byte[length]; - decOutputBuffers[decOutputIndex].clear(); - decOutputBuffers[decOutputIndex].get(mDecodedVideo[j], 0, length); - // Converts the decoded frame to NV21 - convertToNV21(j); - if (j>=NB_DECODED-1) { - flushMediaCodec(mDecoder); - if (VERBOSE) Log.v(TAG, "Decoding "+n+" frames took "+elapsed/1000+" ms"); - return elapsed; - } - j++; - } - mDecoder.releaseOutputBuffer(decOutputIndex, false); - n++; - } - elapsed = timestamp() - now; - } - - throw new RuntimeException("The decoder did not decode anything."); - - } - - /** - * Makes sure the NAL has a header or not. - * @param withPrefix If set to true, the NAL will be preceeded with 0x00000001. - */ - private boolean hasPrefix(byte[] nal) { - if (nal[0] == 0 && nal[1] == 0 && nal[2] == 0 && nal[3] == 0x01) - return true; - else - return false; - } - - private void encodeDecode() { - encode(); - try { - configureDecoder(); - decode(true); - } finally { - releaseDecoder(); - } - } - - private void flushMediaCodec(MediaCodec mc) { - int index = 0; - BufferInfo info = new BufferInfo(); - while (index != MediaCodec.INFO_TRY_AGAIN_LATER) { - index = mc.dequeueOutputBuffer(info, 1000000/FRAMERATE); - if (index>=0) { - mc.releaseOutputBuffer(index, false); - } - } - } - - private void check(boolean cond, String message) { - if (!cond) { - if (VERBOSE) Log.e(TAG,message); - throw new IllegalStateException(message); - } - } - - private long timestamp() { - return System.nanoTime()/1000; - } - -} diff --git a/src/net/majorkernelpanic/streaming/hw/NV21Convertor.java b/src/net/majorkernelpanic/streaming/hw/NV21Convertor.java deleted file mode 100644 index f6cc81a5..00000000 --- a/src/net/majorkernelpanic/streaming/hw/NV21Convertor.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.hw; - -import java.nio.ByteBuffer; - -import android.media.MediaCodecInfo; -import android.util.Log; - -/** - * Converts from NV21 to YUV420 semi planar or planar. - */ -public class NV21Convertor { - - private int mSliceHeight, mHeight; - private int mStride, mWidth; - private int mSize; - private boolean mPlanar, mPanesReversed = false; - private int mYPadding; - private byte[] mBuffer; - ByteBuffer mCopy; - - public void setSize(int width, int height) { - mHeight = height; - mWidth = width; - mSliceHeight = height; - mStride = width; - mSize = mWidth*mHeight; - } - - public void setStride(int width) { - mStride = width; - } - - public void setSliceHeigth(int height) { - mSliceHeight = height; - } - - public void setPlanar(boolean planar) { - mPlanar = planar; - } - - public void setYPadding(int padding) { - mYPadding = padding; - } - - public int getBufferSize() { - return 3*mSize/2; - } - - public void setEncoderColorFormat(int colorFormat) { - switch (colorFormat) { - case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: - case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar: - case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar: - setPlanar(false); - break; - case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: - case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar: - setPlanar(true); - break; - } - } - - public void setColorPanesReversed(boolean b) { - mPanesReversed = b; - } - - public int getStride() { - return mStride; - } - - public int getSliceHeigth() { - return mSliceHeight; - } - - public int getYPadding() { - return mYPadding; - } - - - public boolean getPlanar() { - return mPlanar; - } - - public boolean getUVPanesReversed() { - return mPanesReversed; - } - - public void convert(byte[] data, ByteBuffer buffer) { - byte[] result = convert(data); - int min = buffer.capacity() < data.length?buffer.capacity() : data.length; - buffer.put(result, 0, min); - } - - public byte[] convert(byte[] data) { - - // A buffer large enough for every case - if (mBuffer==null || mBuffer.length != 3*mSliceHeight*mStride/2+mYPadding) { - mBuffer = new byte[3*mSliceHeight*mStride/2+mYPadding]; - } - - if (!mPlanar) { - if (mSliceHeight==mHeight && mStride==mWidth) { - // Swaps U and V - if (!mPanesReversed) { - for (int i = mSize; i < mSize+mSize/2; i += 2) { - mBuffer[0] = data[i+1]; - data[i+1] = data[i]; - data[i] = mBuffer[0]; - } - } - if (mYPadding>0) { - System.arraycopy(data, 0, mBuffer, 0, mSize); - System.arraycopy(data, mSize, mBuffer, mSize+mYPadding, mSize/2); - return mBuffer; - } - return data; - } - } else { - if (mSliceHeight==mHeight && mStride==mWidth) { - // De-interleave U and V - if (!mPanesReversed) { - for (int i = 0; i < mSize/4; i+=1) { - mBuffer[i] = data[mSize+2*i+1]; - mBuffer[mSize/4+i] = data[mSize+2*i]; - } - } else { - for (int i = 0; i < mSize/4; i+=1) { - mBuffer[i] = data[mSize+2*i]; - mBuffer[mSize/4+i] = data[mSize+2*i+1]; - } - } - if (mYPadding == 0) { - System.arraycopy(mBuffer, 0, data, mSize, mSize/2); - } else { - System.arraycopy(data, 0, mBuffer, 0, mSize); - System.arraycopy(mBuffer, 0, mBuffer, mSize+mYPadding, mSize/2); - return mBuffer; - } - return data; - } - } - - return data; - } - -} diff --git a/src/net/majorkernelpanic/streaming/mp4/MP4Config.java b/src/net/majorkernelpanic/streaming/mp4/MP4Config.java deleted file mode 100644 index 3457ce6e..00000000 --- a/src/net/majorkernelpanic/streaming/mp4/MP4Config.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.mp4; -import java.io.FileNotFoundException; -import java.io.IOException; - -import android.util.Base64; -import android.util.Log; - -/** - * Finds SPS & PPS parameters in mp4 file. - */ -public class MP4Config { - - public final static String TAG = "MP4Config"; - - private MP4Parser mp4Parser; - private String mProfilLevel, mPPS, mSPS; - - public MP4Config(String profil, String sps, String pps) { - mProfilLevel = profil; - mPPS = pps; - mSPS = sps; - } - - public MP4Config(String sps, String pps) { - mPPS = pps; - mSPS = sps; - mProfilLevel = MP4Parser.toHexString(Base64.decode(sps, Base64.NO_WRAP),1,3); - } - - public MP4Config(byte[] sps, byte[] pps) { - mPPS = Base64.encodeToString(pps, 0, pps.length, Base64.NO_WRAP); - mSPS = Base64.encodeToString(sps, 0, sps.length, Base64.NO_WRAP); - mProfilLevel = MP4Parser.toHexString(sps,1,3); - } - - /** - * Finds SPS & PPS parameters inside a .mp4. - * @param path Path to the file to analyze - * @throws IOException - * @throws FileNotFoundException - */ - public MP4Config (String path) throws IOException, FileNotFoundException { - - StsdBox stsdBox; - - // We open the mp4 file and parse it - try { - mp4Parser = MP4Parser.parse(path); - } catch (IOException ignore) { - // Maybe enough of the file has been parsed and we can get the stsd box - } - - // We find the stsdBox - stsdBox = mp4Parser.getStsdBox(); - mPPS = stsdBox.getB64PPS(); - mSPS = stsdBox.getB64SPS(); - mProfilLevel = stsdBox.getProfileLevel(); - - mp4Parser.close(); - - } - - public String getProfileLevel() { - return mProfilLevel; - } - - public String getB64PPS() { - Log.d(TAG, "PPS: "+mPPS); - return mPPS; - } - - public String getB64SPS() { - Log.d(TAG, "SPS: "+mSPS); - return mSPS; - } - -} \ No newline at end of file diff --git a/src/net/majorkernelpanic/streaming/mp4/MP4Parser.java b/src/net/majorkernelpanic/streaming/mp4/MP4Parser.java deleted file mode 100644 index 54ad215e..00000000 --- a/src/net/majorkernelpanic/streaming/mp4/MP4Parser.java +++ /dev/null @@ -1,255 +0,0 @@ -package net.majorkernelpanic.streaming.mp4; -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.util.HashMap; - -import android.util.Base64; -import android.util.Log; - -/** - * Parse an mp4 file. - * An mp4 file contains a tree where each node has a name and a size. - * This class is used by H264Stream.java to determine the SPS and PPS parameters of a short video recorded by the phone. - */ -public class MP4Parser { - - private static final String TAG = "MP4Parser"; - - private HashMap mBoxes = new HashMap(); - private final RandomAccessFile mFile; - private long mPos = 0; - - - /** Parses the mp4 file. **/ - public static MP4Parser parse(String path) throws IOException { - return new MP4Parser(path); - } - - private MP4Parser(final String path) throws IOException, FileNotFoundException { - mFile = new RandomAccessFile(new File(path), "r"); - try { - parse("",mFile.length()); - } catch (Exception e) { - e.printStackTrace(); - throw new IOException("Parse error: malformed mp4 file"); - } - } - - public void close() { - try { - mFile.close(); - } catch (Exception e) {}; - } - - public long getBoxPos(String box) throws IOException { - Long r = mBoxes.get(box); - - if (r==null) throw new IOException("Box not found: "+box); - return mBoxes.get(box); - } - - public StsdBox getStsdBox() throws IOException { - try { - return new StsdBox(mFile,getBoxPos("/moov/trak/mdia/minf/stbl/stsd")); - } catch (IOException e) { - throw new IOException("stsd box could not be found"); - } - } - - private void parse(String path, long len) throws IOException { - ByteBuffer byteBuffer; - long sum = 0, newlen = 0; - byte[] buffer = new byte[8]; - String name = ""; - - if(!path.equals("")) mBoxes.put(path, mPos-8); - - while (sum name: "+name+" position: "+mPos+", length: "+newlen); - sum += newlen; - parse(path+'/'+name,newlen); - - } - else { - if( len < 8){ - mFile.seek(mFile.getFilePointer() - 8 + len); - sum += len-8; - } else { - int skipped = mFile.skipBytes((int)(len-8)); - if (skipped < ((int)(len-8))) { - throw new IOException(); - } - mPos += len-8; - sum += len-8; - } - } - } - } - - private boolean validBoxName(byte[] buffer) { - for (int i=0;i<4;i++) { - // If the next 4 bytes are neither lowercase letters nor numbers - if ((buffer[i+4]< 'a' || buffer[i+4]>'z') && (buffer[i+4]<'0'|| buffer[i+4]>'9') ) return false; - } - return true; - } - - static String toHexString(byte[] buffer,int start, int len) { - String c; - StringBuilder s = new StringBuilder(); - for (int i=start;i - * aligned(8) class AVCDecoderConfigurationRecord { - * unsigned int(8) configurationVersion = 1; - * unsigned int(8) AVCProfileIndication; - * unsigned int(8) profile_compatibility; - * unsigned int(8) AVCLevelIndication; - * bit(6) reserved = ‘111111’b; - * unsigned int(2) lengthSizeMinusOne; - * bit(3) reserved = ‘111’b; - * unsigned int(5) numOfSequenceParameterSets; - * for (i=0; i< numOfSequenceParameterSets; i++) { - * unsigned int(16) sequenceParameterSetLength ; - * bit(8*sequenceParameterSetLength) sequenceParameterSetNALUnit; - * } - * unsigned int(8) numOfPictureParameterSets; - * for (i=0; i< numOfPictureParameterSets; i++) { - * unsigned int(16) pictureParameterSetLength; - * bit(8*pictureParameterSetLength) pictureParameterSetNALUnit; - * } - * } - * - */ - try { - - // TODO: Here we assume that numOfSequenceParameterSets = 1, numOfPictureParameterSets = 1 ! - // Here we extract the SPS parameter - fis.skipBytes(7); - spsLength = 0xFF&fis.readByte(); - sps = new byte[spsLength]; - fis.read(sps,0,spsLength); - // Here we extract the PPS parameter - fis.skipBytes(2); - ppsLength = 0xFF&fis.readByte(); - pps = new byte[ppsLength]; - fis.read(pps,0,ppsLength); - - } catch (IOException e) { - return false; - } - - return true; - } - - private boolean findBoxAvcc() { - try { - fis.seek(pos+8); - while (true) { - while (fis.read() != 'a'); - fis.read(buffer,0,3); - if (buffer[0] == 'v' && buffer[1] == 'c' && buffer[2] == 'C') break; - } - } catch (IOException e) { - return false; - } - return true; - - } - -} diff --git a/src/net/majorkernelpanic/streaming/rtcp/SenderReport.java b/src/net/majorkernelpanic/streaming/rtcp/SenderReport.java deleted file mode 100644 index 7f91be04..00000000 --- a/src/net/majorkernelpanic/streaming/rtcp/SenderReport.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.rtcp; - -import static net.majorkernelpanic.streaming.rtp.RtpSocket.TRANSPORT_TCP; -import static net.majorkernelpanic.streaming.rtp.RtpSocket.TRANSPORT_UDP; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.DatagramPacket; -import java.net.InetAddress; -import java.net.MulticastSocket; -import java.nio.channels.IllegalSelectorException; - -import android.os.SystemClock; -import android.util.Log; - -/** - * Implementation of Sender Report RTCP packets. - */ -public class SenderReport { - - public static final int MTU = 1500; - - private static final int PACKET_LENGTH = 28; - - private MulticastSocket usock; - private DatagramPacket upack; - - private int mTransport; - private OutputStream mOutputStream = null; - private byte[] mBuffer = new byte[MTU]; - private int mSSRC, mPort = -1; - private int mOctetCount = 0, mPacketCount = 0; - private long interval, delta, now, oldnow; - private byte mTcpHeader[]; - - public SenderReport(int ssrc) throws IOException { - super(); - this.mSSRC = ssrc; - } - - public SenderReport() { - - mTransport = TRANSPORT_UDP; - mTcpHeader = new byte[] {'$',0,0,PACKET_LENGTH}; - - /* Version(2) Padding(0) */ - /* ^ ^ PT = 0 */ - /* | | ^ */ - /* | -------- | */ - /* | |--------------------- */ - /* | || */ - /* | || */ - mBuffer[0] = (byte) Integer.parseInt("10000000",2); - - /* Packet Type PT */ - mBuffer[1] = (byte) 200; - - /* Byte 2,3 -> Length */ - setLong(PACKET_LENGTH/4-1, 2, 4); - - /* Byte 4,5,6,7 -> SSRC */ - /* Byte 8,9,10,11 -> NTP timestamp hb */ - /* Byte 12,13,14,15 -> NTP timestamp lb */ - /* Byte 16,17,18,19 -> RTP timestamp */ - /* Byte 20,21,22,23 -> packet count */ - /* Byte 24,25,26,27 -> octet count */ - - try { - usock = new MulticastSocket(); - } catch (IOException e) { - // Very unlikely to happen. Means that all UDP ports are already being used - throw new RuntimeException(e.getMessage()); - } - upack = new DatagramPacket(mBuffer, 1); - - // By default we sent one report every 3 secconde - interval = 3000; - - } - - public void close() { - usock.close(); - } - - /** - * Sets the temporal interval between two RTCP Sender Reports. - * Default interval is set to 3 seconds. - * Set 0 to disable RTCP. - * @param interval The interval in milliseconds - */ - public void setInterval(long interval) { - this.interval = interval; - } - - /** - * Updates the number of packets sent, and the total amount of data sent. - * @param length The length of the packet - * @param rtpts - * The RTP timestamp. - * @throws IOException - **/ - public void update(int length, long rtpts) throws IOException { - mPacketCount += 1; - mOctetCount += length; - setLong(mPacketCount, 20, 24); - setLong(mOctetCount, 24, 28); - - now = SystemClock.elapsedRealtime(); - delta += oldnow != 0 ? now-oldnow : 0; - oldnow = now; - if (interval>0) { - if (delta>=interval) { - // We send a Sender Report - send(System.nanoTime(), rtpts); - delta = 0; - } - } - - } - - public void setSSRC(int ssrc) { - this.mSSRC = ssrc; - setLong(ssrc,4,8); - mPacketCount = 0; - mOctetCount = 0; - setLong(mPacketCount, 20, 24); - setLong(mOctetCount, 24, 28); - } - - public void setDestination(InetAddress dest, int dport) { - mTransport = TRANSPORT_UDP; - mPort = dport; - upack.setPort(dport); - upack.setAddress(dest); - } - - /** - * If a TCP is used as the transport protocol for the RTP session, - * the output stream to which RTP packets will be written to must - * be specified with this method. - */ - public void setOutputStream(OutputStream os, byte channelIdentifier) { - mTransport = TRANSPORT_TCP; - mOutputStream = os; - mTcpHeader[1] = channelIdentifier; - } - - public int getPort() { - return mPort; - } - - public int getLocalPort() { - return usock.getLocalPort(); - } - - public int getSSRC() { - return mSSRC; - } - - /** - * Resets the reports (total number of bytes sent, number of packets sent, etc.) - */ - public void reset() { - mPacketCount = 0; - mOctetCount = 0; - setLong(mPacketCount, 20, 24); - setLong(mOctetCount, 24, 28); - delta = now = oldnow = 0; - } - - private void setLong(long n, int begin, int end) { - for (end--; end >= begin; end--) { - mBuffer[end] = (byte) (n % 256); - n >>= 8; - } - } - - /** - * Sends the RTCP packet over the network. - * - * @param ntpts - * the NTP timestamp. - * @param rtpts - * the RTP timestamp. - */ - private void send(long ntpts, long rtpts) throws IOException { - long hb = ntpts/1000000000; - long lb = ( ( ntpts - hb*1000000000 ) * 4294967296L )/1000000000; - setLong(hb, 8, 12); - setLong(lb, 12, 16); - setLong(rtpts, 16, 20); - if (mTransport == TRANSPORT_UDP) { - upack.setLength(PACKET_LENGTH); - usock.send(upack); - } else { - synchronized (mOutputStream) { - try { - mOutputStream.write(mTcpHeader); - mOutputStream.write(mBuffer, 0, PACKET_LENGTH); - } catch (Exception e) {} - } - } - } - - -} diff --git a/src/net/majorkernelpanic/streaming/rtp/AACADTSPacketizer.java b/src/net/majorkernelpanic/streaming/rtp/AACADTSPacketizer.java deleted file mode 100644 index bbff5861..00000000 --- a/src/net/majorkernelpanic/streaming/rtp/AACADTSPacketizer.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.rtp; - -import java.io.IOException; - -import net.majorkernelpanic.streaming.audio.AACStream; -import android.os.SystemClock; -import android.util.Log; - -/** - * - * RFC 3640. - * - * This packetizer must be fed with an InputStream containing ADTS AAC. - * AAC will basically be rewrapped in an RTP stream and sent over the network. - * This packetizer only implements the aac-hbr mode (High Bit-rate AAC) and - * each packet only carry a single and complete AAC access unit. - * - */ -public class AACADTSPacketizer extends AbstractPacketizer implements Runnable { - - private final static String TAG = "AACADTSPacketizer"; - - private Thread t; - private int samplingRate = 8000; - - public AACADTSPacketizer() { - super(); - } - - public void start() { - if (t==null) { - t = new Thread(this); - t.start(); - } - } - - public void stop() { - if (t != null) { - try { - is.close(); - } catch (IOException ignore) {} - t.interrupt(); - try { - t.join(); - } catch (InterruptedException e) {} - t = null; - } - } - - public void setSamplingRate(int samplingRate) { - this.samplingRate = samplingRate; - socket.setClockFrequency(samplingRate); - } - - public void run() { - - Log.d(TAG,"AAC ADTS packetizer started !"); - - // "A packet SHALL carry either one or more complete Access Units, or a - // single fragment of an Access Unit. Fragments of the same Access Unit - // have the same time stamp but different RTP sequence numbers. The - // marker bit in the RTP header is 1 on the last fragment of an Access - // Unit, and 0 on all other fragments." RFC 3640 - - // ADTS header fields that we need to parse - boolean protection; - int frameLength, sum, length, nbau, nbpk, samplingRateIndex, profile; - long oldtime = SystemClock.elapsedRealtime(), now = oldtime; - byte[] header = new byte[8]; - - try { - while (!Thread.interrupted()) { - - // Synchronisation: ADTS packet starts with 12bits set to 1 - while (true) { - if ( (is.read()&0xFF) == 0xFF ) { - header[1] = (byte) is.read(); - if ( (header[1]&0xF0) == 0xF0) break; - } - } - - // Parse adts header (ADTS packets start with a 7 or 9 byte long header) - fill(header, 2, 5); - - // The protection bit indicates whether or not the header contains the two extra bytes - protection = (header[1]&0x01)>0 ? true : false; - frameLength = (header[3]&0x03) << 11 | - (header[4]&0xFF) << 3 | - (header[5]&0xFF) >> 5 ; - frameLength -= (protection ? 7 : 9); - - // Number of AAC frames in the ADTS frame - nbau = (header[6]&0x03) + 1; - - // The number of RTP packets that will be sent for this ADTS frame - nbpk = frameLength/MAXPACKETSIZE + 1; - - // Read CRS if any - if (!protection) is.read(header,0,2); - - samplingRate = AACStream.AUDIO_SAMPLING_RATES[(header[2]&0x3C) >> 2]; - profile = ( (header[2]&0xC0) >> 6 ) + 1 ; - - // We update the RTP timestamp - ts += 1024L*1000000000L/samplingRate; //stats.average(); - - //Log.d(TAG,"frameLength: "+frameLength+" protection: "+protection+" p: "+profile+" sr: "+samplingRate); - - sum = 0; - while (sum MAXPACKETSIZE-rtphl-4) { - length = MAXPACKETSIZE-rtphl-4; - } - else { - length = frameLength-sum; - socket.markNextPacket(); - } - sum += length; - fill(buffer, rtphl+4, length); - - // AU-headers-length field: contains the size in bits of a AU-header - // 13+3 = 16 bits -> 13bits for AU-size and 3bits for AU-Index / AU-Index-delta - // 13 bits will be enough because ADTS uses 13 bits for frame length - buffer[rtphl] = 0; - buffer[rtphl+1] = 0x10; - - // AU-size - buffer[rtphl+2] = (byte) (frameLength>>5); - buffer[rtphl+3] = (byte) (frameLength<<3); - - // AU-Index - buffer[rtphl+3] &= 0xF8; - buffer[rtphl+3] |= 0x00; - - send(rtphl+4+length); - - } - - } - } catch (IOException e) { - // Ignore - } catch (ArrayIndexOutOfBoundsException e) { - Log.e(TAG,"ArrayIndexOutOfBoundsException: "+(e.getMessage()!=null?e.getMessage():"unknown error")); - e.printStackTrace(); - } catch (InterruptedException ignore) {} - - Log.d(TAG,"AAC ADTS packetizer stopped !"); - - } - - private int fill(byte[] buffer, int offset,int length) throws IOException { - int sum = 0, len; - while (sum0) { - - bufferInfo = ((MediaCodecInputStream)is).getLastBufferInfo(); - //Log.d(TAG,"length: "+length+" ts: "+bufferInfo.presentationTimeUs); - oldts = ts; - ts = bufferInfo.presentationTimeUs*1000; - - // Seems to happen sometimes - if (oldts>ts) { - socket.commitBuffer(); - continue; - } - - socket.markNextPacket(); - socket.updateTimestamp(ts); - - // AU-headers-length field: contains the size in bits of a AU-header - // 13+3 = 16 bits -> 13bits for AU-size and 3bits for AU-Index / AU-Index-delta - // 13 bits will be enough because ADTS uses 13 bits for frame length - buffer[rtphl] = 0; - buffer[rtphl+1] = 0x10; - - // AU-size - buffer[rtphl+2] = (byte) (length>>5); - buffer[rtphl+3] = (byte) (length<<3); - - // AU-Index - buffer[rtphl+3] &= 0xF8; - buffer[rtphl+3] |= 0x00; - - send(rtphl+length+4); - - } else { - socket.commitBuffer(); - } - - } - } catch (IOException e) { - } catch (ArrayIndexOutOfBoundsException e) { - Log.e(TAG,"ArrayIndexOutOfBoundsException: "+(e.getMessage()!=null?e.getMessage():"unknown error")); - e.printStackTrace(); - } catch (InterruptedException ignore) {} - - Log.d(TAG,"AAC LATM packetizer stopped !"); - - } - -} diff --git a/src/net/majorkernelpanic/streaming/rtp/AMRNBPacketizer.java b/src/net/majorkernelpanic/streaming/rtp/AMRNBPacketizer.java deleted file mode 100644 index 5ad14639..00000000 --- a/src/net/majorkernelpanic/streaming/rtp/AMRNBPacketizer.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.rtp; - -import java.io.IOException; - -import android.util.Log; - -/** - * - * RFC 3267. - * - * AMR Streaming over RTP. - * - * Must be fed with an InputStream containing raw AMR NB - * Stream must begin with a 6 bytes long header: "#!AMR\n", it will be skipped - * - */ -public class AMRNBPacketizer extends AbstractPacketizer implements Runnable { - - public final static String TAG = "AMRNBPacketizer"; - - private final int AMR_HEADER_LENGTH = 6; // "#!AMR\n" - private static final int AMR_FRAME_HEADER_LENGTH = 1; // Each frame has a short header - private static final int[] sFrameBits = {95, 103, 118, 134, 148, 159, 204, 244}; - private int samplingRate = 8000; - - private Thread t; - - public AMRNBPacketizer() { - super(); - socket.setClockFrequency(samplingRate); - } - - public void start() { - if (t==null) { - t = new Thread(this); - t.start(); - } - } - - public void stop() { - if (t != null) { - try { - is.close(); - } catch (IOException ignore) {} - t.interrupt(); - try { - t.join(); - } catch (InterruptedException e) {} - t = null; - } - } - - public void run() { - - int frameLength, frameType; - long now = System.nanoTime(), oldtime = now; - byte[] header = new byte[AMR_HEADER_LENGTH]; - - try { - - // Skip raw AMR header - fill(header,0,AMR_HEADER_LENGTH); - - if (header[5] != '\n') { - Log.e(TAG,"Bad header ! AMR not correcty supported by the phone !"); - return; - } - - while (!Thread.interrupted()) { - - buffer = socket.requestBuffer(); - buffer[rtphl] = (byte) 0xF0; - - // First we read the frame header - fill(buffer, rtphl+1,AMR_FRAME_HEADER_LENGTH); - - // Then we calculate the frame payload length - frameType = (Math.abs(buffer[rtphl + 1]) >> 3) & 0x0f; - frameLength = (sFrameBits[frameType]+7)/8; - - // And we read the payload - fill(buffer, rtphl+2,frameLength); - - //Log.d(TAG,"Frame length: "+frameLength+" frameType: "+frameType); - - // RFC 3267 Page 14: "For AMR, the sampling frequency is 8 kHz" - // FIXME: Is this really always the case ?? - ts += 160L*1000000000L/samplingRate; //stats.average(); - socket.updateTimestamp(ts); - socket.markNextPacket(); - - //Log.d(TAG,"expected: "+ expected + " measured: "+measured); - - send(rtphl+1+AMR_FRAME_HEADER_LENGTH+frameLength); - - } - - } catch (IOException e) { - } catch (InterruptedException e) {} - - Log.d(TAG,"AMR packetizer stopped !"); - - } - - private int fill(byte[] buffer, int offset,int length) throws IOException { - int sum = 0, len; - while (sumperiod) { - elapsed = 0; - long now = System.nanoTime(); - if (!initoffset || (now - start < 0)) { - start = now; - duration = 0; - initoffset = true; - } - // Prevents drifting issues by comparing the real duration of the - // stream with the sum of all temporal lengths of RTP packets. - value += (now - start) - duration; - //Log.d(TAG, "sum1: "+duration/1000000+" sum2: "+(now-start)/1000000+" drift: "+((now-start)-duration)/1000000+" v: "+value/1000000); - } - if (c<5) { - // We ignore the first 20 measured values because they may not be accurate - c++; - m = value; - } else { - m = (m*q+value)/(q+1); - if (q>2; - //Log.d(TAG,"j: "+j+" buffer: "+printBuffer(rtphl, rtphl+5)+" tr: "+tr); - if (firstFragment) { - // This is the first fragment of the frame -> header is set to 0x0400 - buffer[rtphl] = 4; - firstFragment = false; - } else { - buffer[rtphl] = 0; - } - if (j>0) { - // We have found the end of the frame - stats.push(duration); - ts+= stats.average(); duration = 0; - //Log.d(TAG,"End of frame ! duration: "+stats.average()); - // The last fragment of a frame has to be marked - socket.markNextPacket(); - send(j); - nextBuffer = socket.requestBuffer(); - System.arraycopy(buffer,j+2,nextBuffer,rtphl+2,MAXPACKETSIZE-j-2); - buffer = nextBuffer; - j = MAXPACKETSIZE-j-2; - firstFragment = true; - } else { - // We have not found the beginning of another frame - // The whole packet is a fragment of a frame - send(MAXPACKETSIZE); - } - } - } catch (IOException e) { - } catch (InterruptedException e) {} - - Log.d(TAG,"H263 Packetizer stopped !"); - - } - - private int fill(int offset,int length) throws IOException { - - int sum = 0, len; - - while (sum>8); - stapa[2] = (byte) (sps.length&0xFF); - stapa[sps.length+1] = (byte) (pps.length>>8); - stapa[sps.length+2] = (byte) (pps.length&0xFF); - System.arraycopy(sps, 0, stapa, 3, sps.length); - System.arraycopy(pps, 0, stapa, 5+sps.length, pps.length); - } - } - - public void run() { - long duration = 0; - Log.d(TAG,"H264 packetizer started !"); - stats.reset(); - count = 0; - - if (is instanceof MediaCodecInputStream) { - streamType = 1; - socket.setCacheSize(0); - } else { - streamType = 0; - socket.setCacheSize(400); - } - - try { - while (!Thread.interrupted()) { - - oldtime = System.nanoTime(); - // We read a NAL units from the input stream and we send them - send(); - // We measure how long it took to receive NAL units from the phone - duration = System.nanoTime() - oldtime; - - stats.push(duration); - // Computes the average duration of a NAL unit - delay = stats.average(); - //Log.d(TAG,"duration: "+duration/1000000+" delay: "+delay/1000000); - - } - } catch (IOException e) { - } catch (InterruptedException e) {} - - Log.d(TAG,"H264 packetizer stopped !"); - - } - - /** - * Reads a NAL unit in the FIFO and sends it. - * If it is too big, we split it in FU-A units (RFC 3984). - */ - @SuppressLint("NewApi") - private void send() throws IOException, InterruptedException { - int sum = 1, len = 0, type; - - if (streamType == 0) { - // NAL units are preceeded by their length, we parse the length - fill(header,0,5); - ts += delay; - naluLength = header[3]&0xFF | (header[2]&0xFF)<<8 | (header[1]&0xFF)<<16 | (header[0]&0xFF)<<24; - if (naluLength>100000 || naluLength<0) resync(); - } else if (streamType == 1) { - // NAL units are preceeded with 0x00000001 - fill(header,0,5); - ts = ((MediaCodecInputStream)is).getLastBufferInfo().presentationTimeUs*1000L; - //ts += delay; - naluLength = is.available()+1; - if (!(header[0]==0 && header[1]==0 && header[2]==0)) { - // Turns out, the NAL units are not preceeded with 0x00000001 - Log.e(TAG, "NAL units are not preceeded by 0x00000001"); - streamType = 2; - return; - } - } else { - // Nothing preceededs the NAL units - fill(header,0,1); - header[4] = header[0]; - ts = ((MediaCodecInputStream)is).getLastBufferInfo().presentationTimeUs*1000L; - //ts += delay; - naluLength = is.available()+1; - } - - // Parses the NAL unit type - type = header[4]&0x1F; - - - // The stream already contains NAL unit type 7 or 8, we don't need - // to add them to the stream ourselves - if (type == 7 || type == 8) { - Log.v(TAG,"SPS or PPS present in the stream."); - count++; - if (count>4) { - sps = null; - pps = null; - } - } - - // We send two packets containing NALU type 7 (SPS) and 8 (PPS) - // Those should allow the H264 stream to be decoded even if no SDP was sent to the decoder. - if (type == 5 && sps != null && pps != null) { - buffer = socket.requestBuffer(); - socket.markNextPacket(); - socket.updateTimestamp(ts); - System.arraycopy(stapa, 0, buffer, rtphl, stapa.length); - super.send(rtphl+stapa.length); - } - - //Log.d(TAG,"- Nal unit length: " + naluLength + " delay: "+delay/1000000+" type: "+type); - - // Small NAL unit => Single NAL unit - if (naluLength<=MAXPACKETSIZE-rtphl-2) { - buffer = socket.requestBuffer(); - buffer[rtphl] = header[4]; - len = fill(buffer, rtphl+1, naluLength-1); - socket.updateTimestamp(ts); - socket.markNextPacket(); - super.send(naluLength+rtphl); - //Log.d(TAG,"----- Single NAL unit - len:"+len+" delay: "+delay); - } - // Large NAL unit => Split nal unit - else { - - // Set FU-A header - header[1] = (byte) (header[4] & 0x1F); // FU header type - header[1] += 0x80; // Start bit - // Set FU-A indicator - header[0] = (byte) ((header[4] & 0x60) & 0xFF); // FU indicator NRI - header[0] += 28; - - while (sum < naluLength) { - buffer = socket.requestBuffer(); - buffer[rtphl] = header[0]; - buffer[rtphl+1] = header[1]; - socket.updateTimestamp(ts); - if ((len = fill(buffer, rtphl+2, naluLength-sum > MAXPACKETSIZE-rtphl-2 ? MAXPACKETSIZE-rtphl-2 : naluLength-sum ))<0) return; sum += len; - // Last packet before next NAL - if (sum >= naluLength) { - // End bit on - buffer[rtphl+1] += 0x40; - socket.markNextPacket(); - } - super.send(len+rtphl+2); - // Switch start bit - header[1] = (byte) (header[1] & 0x7F); - //Log.d(TAG,"----- FU-A unit, sum:"+sum); - } - } - } - - private int fill(byte[] buffer, int offset,int length) throws IOException { - int sum = 0, len; - while (sum0 && naluLength<100000) { - oldtime = System.nanoTime(); - Log.e(TAG,"A NAL unit may have been found in the bit stream !"); - break; - } - if (naluLength==0) { - Log.e(TAG,"NAL unit with NULL size found..."); - } else if (header[3]==0xFF && header[2]==0xFF && header[1]==0xFF && header[0]==0xFF) { - Log.e(TAG,"NAL unit with 0xFFFFFFFF size found..."); - } - } - - } - - } - -} \ No newline at end of file diff --git a/src/net/majorkernelpanic/streaming/rtp/MediaCodecInputStream.java b/src/net/majorkernelpanic/streaming/rtp/MediaCodecInputStream.java deleted file mode 100644 index b77e0f69..00000000 --- a/src/net/majorkernelpanic/streaming/rtp/MediaCodecInputStream.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.rtp; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -import android.annotation.SuppressLint; -import android.media.MediaCodec; -import android.media.MediaCodec.BufferInfo; -import android.media.MediaFormat; -import android.util.Log; - -/** - * An InputStream that uses data from a MediaCodec. - * The purpose of this class is to interface existing RTP packetizers of - * libstreaming with the new MediaCodec API. This class is not thread safe ! - */ -@SuppressLint("NewApi") -public class MediaCodecInputStream extends InputStream { - - public final String TAG = "MediaCodecInputStream"; - - private MediaCodec mMediaCodec = null; - private BufferInfo mBufferInfo = new BufferInfo(); - private ByteBuffer[] mBuffers = null; - private ByteBuffer mBuffer = null; - private int mIndex = -1; - private boolean mClosed = false; - - public MediaFormat mMediaFormat; - - public MediaCodecInputStream(MediaCodec mediaCodec) { - mMediaCodec = mediaCodec; - mBuffers = mMediaCodec.getOutputBuffers(); - } - - @Override - public void close() { - mClosed = true; - } - - @Override - public int read() throws IOException { - return 0; - } - - @Override - public int read(byte[] buffer, int offset, int length) throws IOException { - int min = 0; - - try { - if (mBuffer==null) { - while (!Thread.interrupted() && !mClosed) { - mIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 500000); - if (mIndex>=0 ){ - //Log.d(TAG,"Index: "+mIndex+" Time: "+mBufferInfo.presentationTimeUs+" size: "+mBufferInfo.size); - mBuffer = mBuffers[mIndex]; - mBuffer.position(0); - break; - } else if (mIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { - mBuffers = mMediaCodec.getOutputBuffers(); - } else if (mIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - mMediaFormat = mMediaCodec.getOutputFormat(); - Log.i(TAG,mMediaFormat.toString()); - } else if (mIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { - Log.v(TAG,"No buffer available..."); - //return 0; - } else { - Log.e(TAG,"Message: "+mIndex); - //return 0; - } - } - } - - if (mClosed) throw new IOException("This InputStream was closed"); - - min = length < mBufferInfo.size - mBuffer.position() ? length : mBufferInfo.size - mBuffer.position(); - mBuffer.get(buffer, offset, min); - if (mBuffer.position()>=mBufferInfo.size) { - mMediaCodec.releaseOutputBuffer(mIndex, false); - mBuffer = null; - } - - } catch (RuntimeException e) { - e.printStackTrace(); - } - - return min; - } - - public int available() { - if (mBuffer != null) - return mBufferInfo.size - mBuffer.position(); - else - return 0; - } - - public BufferInfo getLastBufferInfo() { - return mBufferInfo; - } - -} diff --git a/src/net/majorkernelpanic/streaming/rtp/RtpSocket.java b/src/net/majorkernelpanic/streaming/rtp/RtpSocket.java deleted file mode 100644 index 9ae91e95..00000000 --- a/src/net/majorkernelpanic/streaming/rtp/RtpSocket.java +++ /dev/null @@ -1,451 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.rtp; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.DatagramPacket; -import java.net.InetAddress; -import java.net.MulticastSocket; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; - -import net.majorkernelpanic.streaming.rtcp.SenderReport; -import android.os.SystemClock; -import android.util.Log; - -/** - * A basic implementation of an RTP socket. - * It implements a buffering mechanism, relying on a FIFO of buffers and a Thread. - * That way, if a packetizer tries to send many packets too quickly, the FIFO will - * grow and packets will be sent one by one smoothly. - */ -public class RtpSocket implements Runnable { - - public static final String TAG = "RtpSocket"; - - /** Use this to use UDP for the transport protocol. */ - public final static int TRANSPORT_UDP = 0x00; - - /** Use this to use TCP for the transport protocol. */ - public final static int TRANSPORT_TCP = 0x01; - - public static final int RTP_HEADER_LENGTH = 12; - public static final int MTU = 1300; - - private MulticastSocket mSocket; - private DatagramPacket[] mPackets; - private byte[][] mBuffers; - private long[] mTimestamps; - - private SenderReport mReport; - - private Semaphore mBufferRequested, mBufferCommitted; - private Thread mThread; - - private int mTransport; - private long mCacheSize; - private long mClock = 0; - private long mOldTimestamp = 0; - private int mSsrc, mSeq = 0, mPort = -1; - private int mBufferCount, mBufferIn, mBufferOut; - private int mCount = 0; - private byte mTcpHeader[]; - protected OutputStream mOutputStream = null; - - private AverageBitrate mAverageBitrate; - - /** - * This RTP socket implements a buffering mechanism relying on a FIFO of buffers and a Thread. - * @throws IOException - */ - public RtpSocket() { - - mCacheSize = 0; - mBufferCount = 300; // TODO: readjust that when the FIFO is full - mBuffers = new byte[mBufferCount][]; - mPackets = new DatagramPacket[mBufferCount]; - mReport = new SenderReport(); - mAverageBitrate = new AverageBitrate(); - mTransport = TRANSPORT_UDP; - mTcpHeader = new byte[] {'$',0,0,0}; - - resetFifo(); - - for (int i=0; i Source Identifier(0) */ - /* | || | */ - mBuffers[i][0] = (byte) Integer.parseInt("10000000",2); - - /* Payload Type */ - mBuffers[i][1] = (byte) 96; - - /* Byte 2,3 -> Sequence Number */ - /* Byte 4,5,6,7 -> Timestamp */ - /* Byte 8,9,10,11 -> Sync Source Identifier */ - - } - - try { - mSocket = new MulticastSocket(); - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); - } - - } - - private void resetFifo() { - mCount = 0; - mBufferIn = 0; - mBufferOut = 0; - mTimestamps = new long[mBufferCount]; - mBufferRequested = new Semaphore(mBufferCount); - mBufferCommitted = new Semaphore(0); - mReport.reset(); - mAverageBitrate.reset(); - } - - /** Closes the underlying socket. */ - public void close() { - mSocket.close(); - } - - /** Sets the SSRC of the stream. */ - public void setSSRC(int ssrc) { - this.mSsrc = ssrc; - for (int i=0;i=mBufferCount) mBufferIn = 0; - mBufferCommitted.release(); - - } - - /** Sends the RTP packet over the network. */ - public void commitBuffer(int length) throws IOException { - updateSequence(); - mPackets[mBufferIn].setLength(length); - - mAverageBitrate.push(length); - - if (++mBufferIn>=mBufferCount) mBufferIn = 0; - mBufferCommitted.release(); - - if (mThread == null) { - mThread = new Thread(this); - mThread.start(); - } - - } - - /** Returns an approximation of the bitrate of the RTP stream in bits per second. */ - public long getBitrate() { - return mAverageBitrate.average(); - } - - /** Increments the sequence number. */ - private void updateSequence() { - setLong(mBuffers[mBufferIn], ++mSeq, 2, 4); - } - - /** - * Overwrites the timestamp in the packet. - * @param timestamp The new timestamp in ns. - **/ - public void updateTimestamp(long timestamp) { - mTimestamps[mBufferIn] = timestamp; - setLong(mBuffers[mBufferIn], (timestamp/100L)*(mClock/1000L)/10000L, 4, 8); - } - - /** Sets the marker in the RTP packet. */ - public void markNextPacket() { - mBuffers[mBufferIn][1] |= 0x80; - } - - /** The Thread sends the packets in the FIFO one by one at a constant rate. */ - @Override - public void run() { - Statistics stats = new Statistics(50,3000); - try { - // Caches mCacheSize milliseconds of the stream in the FIFO. - Thread.sleep(mCacheSize); - long delta = 0; - while (mBufferCommitted.tryAcquire(4,TimeUnit.SECONDS)) { - if (mOldTimestamp != 0) { - // We use our knowledge of the clock rate of the stream and the difference between two timestamps to - // compute the time lapse that the packet represents. - if ((mTimestamps[mBufferOut]-mOldTimestamp)>0) { - stats.push(mTimestamps[mBufferOut]-mOldTimestamp); - long d = stats.average()/1000000; - //Log.d(TAG,"delay: "+d+" d: "+(mTimestamps[mBufferOut]-mOldTimestamp)/1000000); - // We ensure that packets are sent at a constant and suitable rate no matter how the RtpSocket is used. - if (mCacheSize>0) Thread.sleep(d); - } else if ((mTimestamps[mBufferOut]-mOldTimestamp)<0) { - Log.e(TAG, "TS: "+mTimestamps[mBufferOut]+" OLD: "+mOldTimestamp); - } - delta += mTimestamps[mBufferOut]-mOldTimestamp; - if (delta>500000000 || delta<0) { - //Log.d(TAG,"permits: "+mBufferCommitted.availablePermits()); - delta = 0; - } - } - mReport.update(mPackets[mBufferOut].getLength(), (mTimestamps[mBufferOut]/100L)*(mClock/1000L)/10000L); - mOldTimestamp = mTimestamps[mBufferOut]; - if (mCount++>30) { - if (mTransport == TRANSPORT_UDP) { - mSocket.send(mPackets[mBufferOut]); - } else { - sendTCP(); - } - } - if (++mBufferOut>=mBufferCount) mBufferOut = 0; - mBufferRequested.release(); - } - } catch (Exception e) { - e.printStackTrace(); - } - mThread = null; - resetFifo(); - } - - private void sendTCP() { - synchronized (mOutputStream) { - int len = mPackets[mBufferOut].getLength(); - Log.d(TAG,"sent "+len); - mTcpHeader[2] = (byte) (len>>8); - mTcpHeader[3] = (byte) (len&0xFF); - try { - mOutputStream.write(mTcpHeader); - mOutputStream.write(mBuffers[mBufferOut], 0, len); - } catch (Exception e) {} - } - } - - private void setLong(byte[] buffer, long n, int begin, int end) { - for (end--; end >= begin; end--) { - buffer[end] = (byte) (n % 256); - n >>= 8; - } - } - - /** - * Computes an average bit rate. - **/ - protected static class AverageBitrate { - - private final static long RESOLUTION = 200; - - private long mOldNow, mNow, mDelta; - private long[] mElapsed, mSum; - private int mCount, mIndex, mTotal; - private int mSize; - - public AverageBitrate() { - mSize = 5000/((int)RESOLUTION); - reset(); - } - - public AverageBitrate(int delay) { - mSize = delay/((int)RESOLUTION); - reset(); - } - - public void reset() { - mSum = new long[mSize]; - mElapsed = new long[mSize]; - mNow = SystemClock.elapsedRealtime(); - mOldNow = mNow; - mCount = 0; - mDelta = 0; - mTotal = 0; - mIndex = 0; - } - - public void push(int length) { - mNow = SystemClock.elapsedRealtime(); - if (mCount>0) { - mDelta += mNow - mOldNow; - mTotal += length; - if (mDelta>RESOLUTION) { - mSum[mIndex] = mTotal; - mTotal = 0; - mElapsed[mIndex] = mDelta; - mDelta = 0; - mIndex++; - if (mIndex>=mSize) mIndex = 0; - } - } - mOldNow = mNow; - mCount++; - } - - public int average() { - long delta = 0, sum = 0; - for (int i=0;i0?8000*sum/delta:0); - } - - } - - /** Computes the proper rate at which packets are sent. */ - protected static class Statistics { - - public final static String TAG = "Statistics"; - - private int count=500, c = 0; - private float m = 0, q = 0; - private long elapsed = 0; - private long start = 0; - private long duration = 0; - private long period = 6000000000L; - private boolean initoffset = false; - - public Statistics(int count, long period) { - this.count = count; - this.period = period*1000000L; - } - - public void push(long value) { - duration += value; - elapsed += value; - if (elapsed>period) { - elapsed = 0; - long now = System.nanoTime(); - if (!initoffset || (now - start < 0)) { - start = now; - duration = 0; - initoffset = true; - } - value -= (now - start) - duration; - //Log.d(TAG, "sum1: "+duration/1000000+" sum2: "+(now-start)/1000000+" drift: "+((now-start)-duration)/1000000+" v: "+value/1000000); - } - if (c<40) { - // We ignore the first 40 measured values because they may not be accurate - c++; - m = value; - } else { - m = (m*q+value)/(q+1); - if (q0 ? l : 0; - } - - } - -} diff --git a/src/net/majorkernelpanic/streaming/rtsp/RtcpDeinterleaver.java b/src/net/majorkernelpanic/streaming/rtsp/RtcpDeinterleaver.java deleted file mode 100644 index 8bfc3b56..00000000 --- a/src/net/majorkernelpanic/streaming/rtsp/RtcpDeinterleaver.java +++ /dev/null @@ -1,72 +0,0 @@ -package net.majorkernelpanic.streaming.rtsp; - -import java.io.IOException; -import java.io.InputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; - -class RtcpDeinterleaver extends InputStream implements Runnable { - - public final static String TAG = "RtcpDeinterleaver"; - - private IOException mIOException; - private InputStream mInputStream; - private PipedInputStream mPipedInputStream; - private PipedOutputStream mPipedOutputStream; - private byte[] mBuffer; - - public RtcpDeinterleaver(InputStream inputStream) { - mInputStream = inputStream; - mPipedInputStream = new PipedInputStream(4096); - try { - mPipedOutputStream = new PipedOutputStream(mPipedInputStream); - } catch (IOException e) {} - mBuffer = new byte[1024]; - new Thread(this).start(); - } - - @Override - public void run() { - try { - while (true) { - int len = mInputStream.read(mBuffer, 0, 1024); - mPipedOutputStream.write(mBuffer, 0, len); - } - } catch (IOException e) { - try { - mPipedInputStream.close(); - } catch (IOException ignore) {} - mIOException = e; - } - } - - @Override - public int read(byte[] buffer) throws IOException { - if (mIOException != null) { - throw mIOException; - } - return mPipedInputStream.read(buffer); - } - - @Override - public int read(byte[] buffer, int offset, int length) throws IOException { - if (mIOException != null) { - throw mIOException; - } - return mPipedInputStream.read(buffer, offset, length); - } - - @Override - public int read() throws IOException { - if (mIOException != null) { - throw mIOException; - } - return mPipedInputStream.read(); - } - - @Override - public void close() throws IOException { - mInputStream.close(); - } - -} diff --git a/src/net/majorkernelpanic/streaming/rtsp/RtspClient.java b/src/net/majorkernelpanic/streaming/rtsp/RtspClient.java deleted file mode 100644 index d85fd4ec..00000000 --- a/src/net/majorkernelpanic/streaming/rtsp/RtspClient.java +++ /dev/null @@ -1,607 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.rtsp; - -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.Socket; -import java.net.SocketException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.HashMap; -import java.util.Locale; -import java.util.concurrent.Semaphore; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import net.majorkernelpanic.streaming.Session; -import net.majorkernelpanic.streaming.Stream; -import net.majorkernelpanic.streaming.rtp.RtpSocket; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.util.Log; - -/** - * RFC 2326. - * A basic and asynchronous RTSP client. - * The original purpose of this class was to implement a small RTSP client compatible with Wowza. - * It implements Digest Access Authentication according to RFC 2069. - */ -public class RtspClient { - - public final static String TAG = "RtspClient"; - - /** Message sent when the connection to the RTSP server failed. */ - public final static int ERROR_CONNECTION_FAILED = 0x01; - - /** Message sent when the credentials are wrong. */ - public final static int ERROR_WRONG_CREDENTIALS = 0x03; - - /** Use this to use UDP for the transport protocol. */ - public final static int TRANSPORT_UDP = RtpSocket.TRANSPORT_UDP; - - /** Use this to use TCP for the transport protocol. */ - public final static int TRANSPORT_TCP = RtpSocket.TRANSPORT_TCP; - - /** - * Message sent when the connection with the RTSP server has been lost for - * some reason (for example, the user is going under a bridge). - * When the connection with the server is lost, the client will automatically try to - * reconnect as long as {@link #stopStream()} is not called. - **/ - public final static int ERROR_CONNECTION_LOST = 0x04; - - /** - * Message sent when the connection with the RTSP server has been reestablished. - * When the connection with the server is lost, the client will automatically try to - * reconnect as long as {@link #stopStream()} is not called. - */ - public final static int MESSAGE_CONNECTION_RECOVERED = 0x05; - - private final static int STATE_STARTED = 0x00; - private final static int STATE_STARTING = 0x01; - private final static int STATE_STOPPING = 0x02; - private final static int STATE_STOPPED = 0x03; - private int mState = 0; - - private class Parameters { - public String host; - public String username; - public String password; - public String path; - public Session session; - public int port; - public int transport; - - public Parameters clone() { - Parameters params = new Parameters(); - params.host = host; - params.username = username; - params.password = password; - params.path = path; - params.session = session; - params.port = port; - params.transport = transport; - return params; - } - } - - - private Parameters mTmpParameters; - private Parameters mParameters; - - private int mCSeq; - private Socket mSocket; - private String mSessionID; - private String mAuthorization; - private BufferedReader mBufferedReader; - private OutputStream mOutputStream; - private Callback mCallback; - private Handler mMainHandler; - private Handler mHandler; - - /** - * The callback interface you need to implement to know what's going on with the - * RTSP server (for example your Wowza Media Server). - */ - public interface Callback { - public void onRtspUpdate(int message, Exception exception); - } - - public RtspClient() { - mCSeq = 0; - mTmpParameters = new Parameters(); - mTmpParameters.port = 1935; - mTmpParameters.path = "/"; - mTmpParameters.transport = TRANSPORT_UDP; - mAuthorization = null; - mCallback = null; - mMainHandler = new Handler(Looper.getMainLooper()); - mState = STATE_STOPPED; - - final Semaphore signal = new Semaphore(0); - new HandlerThread("net.majorkernelpanic.streaming.RtspClient"){ - @Override - protected void onLooperPrepared() { - mHandler = new Handler(); - signal.release(); - } - }.start(); - signal.acquireUninterruptibly(); - - } - - /** - * Sets the callback interface that will be called on status updates of the connection - * with the RTSP server. - * @param cb The implementation of the {@link Callback} interface - */ - public void setCallback(Callback cb) { - mCallback = cb; - } - - /** - * The {@link Session} that will be used to stream to the server. - * If not called before {@link #startStream()}, a it will be created. - */ - public void setSession(Session session) { - mTmpParameters.session = session; - } - - public Session getSession() { - return mTmpParameters.session; - } - - /** - * Sets the destination address of the RTSP server. - * @param host The destination address - * @param port The destination port - */ - public void setServerAddress(String host, int port) { - mTmpParameters.port = port; - mTmpParameters.host = host; - } - - /** - * If authentication is enabled on the server, you need to call this with a valid username/password pair. - * Only implements Digest Access Authentication according to RFC 2069. - * @param username The username - * @param password The password - */ - public void setCredentials(String username, String password) { - mTmpParameters.username = username; - mTmpParameters.password = password; - } - - /** - * The path to which the stream will be sent to. - * @param path The path - */ - public void setStreamPath(String path) { - mTmpParameters.path = path; - } - - /** - * Call this with {@link #TRANSPORT_TCP} or {@value #TRANSPORT_UDP} to choose the - * transport protocol that will be used to send RTP/RTCP packets. - * Not ready yet ! - */ - public void setTransportMode(int mode) { - mTmpParameters.transport = mode; - } - - public boolean isStreaming() { - return mState==STATE_STARTED|mState==STATE_STARTING; - } - - /** - * Connects to the RTSP server to publish the stream, and the effectively starts streaming. - * You need to call {@link #setServerAddress(String, int)} and optionnally {@link #setSession(Session)} - * and {@link #setCredentials(String, String)} before calling this. - * Should be called of the main thread ! - */ - public void startStream() { - if (mTmpParameters.host == null) throw new IllegalStateException("setServerAddress(String,int) has not been called !"); - if (mTmpParameters.session == null) throw new IllegalStateException("setSession() has not been called !"); - mHandler.post(new Runnable () { - @Override - public void run() { - if (mState != STATE_STOPPED) return; - mState = STATE_STARTING; - - Log.d(TAG,"Connecting to RTSP server..."); - - // If the user calls some methods to configure the client, it won't modify its behavior until the stream is restarted - mParameters = mTmpParameters.clone(); - mParameters.session.setDestination(mTmpParameters.host); - - try { - mParameters.session.syncConfigure(); - } catch (Exception e) { - mParameters.session = null; - mState = STATE_STOPPED; - return; - } - - try { - tryConnection(); - } catch (Exception e) { - postError(ERROR_CONNECTION_FAILED, e); - abort(); - return; - } - - try { - mParameters.session.syncStart(); - mState = STATE_STARTED; - if (mParameters.transport == TRANSPORT_UDP) { - mHandler.post(mConnectionMonitor); - } - } catch (Exception e) { - abort(); - } - - } - }); - - } - - /** - * Stops the stream, and informs the RTSP server. - */ - public void stopStream() { - mHandler.post(new Runnable () { - @Override - public void run() { - if (mParameters != null && mParameters.session != null) { - mParameters.session.stop(); - } - if (mState != STATE_STOPPED) { - mState = STATE_STOPPING; - abort(); - } - } - }); - } - - public void release() { - stopStream(); - mHandler.getLooper().quit(); - } - - private void abort() { - try { - sendRequestTeardown(); - } catch (Exception ignore) {} - try { - mSocket.close(); - } catch (Exception ignore) {} - mHandler.removeCallbacks(mConnectionMonitor); - mHandler.removeCallbacks(mRetryConnection); - mState = STATE_STOPPED; - } - - private void tryConnection() throws IOException { - mCSeq = 0; - mSocket = new Socket(mParameters.host, mParameters.port); - mBufferedReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream())); - mOutputStream = new BufferedOutputStream(mSocket.getOutputStream()); - sendRequestAnnounce(); - sendRequestSetup(); - sendRequestRecord(); - } - - /** - * Forges and sends the ANNOUNCE request - */ - private void sendRequestAnnounce() throws IllegalStateException, SocketException, IOException { - - String body = mParameters.session.getSessionDescription(); - String request = "ANNOUNCE rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + - "CSeq: " + (++mCSeq) + "\r\n" + - "Content-Length: " + body.length() + "\r\n" + - "Content-Type: application/sdp \r\n\r\n" + - body; - Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); - - mOutputStream.write(request.getBytes("UTF-8")); - mOutputStream.flush(); - Response response = Response.parseResponse(mBufferedReader); - - if (response.headers.containsKey("server")) { - Log.v(TAG,"RTSP server name:" + response.headers.get("server")); - } else { - Log.v(TAG,"RTSP server name unknown"); - } - - try { - Matcher m = Response.rexegSession.matcher(response.headers.get("session")); - m.find(); - mSessionID = m.group(1); - } catch (Exception e) { - throw new IOException("Invalid response from server. Session id: "+mSessionID); - } - - if (response.status == 401) { - String nonce, realm; - Matcher m; - - if (mParameters.username == null || mParameters.password == null) throw new IllegalStateException("Authentication is enabled and setCredentials(String,String) was not called !"); - - try { - m = Response.rexegAuthenticate.matcher(response.headers.get("www-authenticate")); m.find(); - nonce = m.group(2); - realm = m.group(1); - } catch (Exception e) { - throw new IOException("Invalid response from server"); - } - - String uri = "rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path; - String hash1 = computeMd5Hash(mParameters.username+":"+m.group(1)+":"+mParameters.password); - String hash2 = computeMd5Hash("ANNOUNCE"+":"+uri); - String hash3 = computeMd5Hash(hash1+":"+m.group(2)+":"+hash2); - - mAuthorization = "Digest username=\""+mParameters.username+"\",realm=\""+realm+"\",nonce=\""+nonce+"\",uri=\""+uri+"\",response=\""+hash3+"\""; - - request = "ANNOUNCE rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + - "CSeq: " + (++mCSeq) + "\r\n" + - "Content-Length: " + body.length() + "\r\n" + - "Authorization: " + mAuthorization + "\r\n" + - "Session: " + mSessionID + "\r\n" + - "Content-Type: application/sdp \r\n\r\n" + - body; - - Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); - - mOutputStream.write(request.getBytes("UTF-8")); - mOutputStream.flush(); - response = Response.parseResponse(mBufferedReader); - - if (response.status == 401) throw new RuntimeException("Bad credentials !"); - - } else if (response.status == 403) { - throw new RuntimeException("Access forbidden !"); - } - - } - - /** - * Forges and sends the SETUP request - */ - private void sendRequestSetup() throws IllegalStateException, SocketException, IOException { - for (int i=0;i<2;i++) { - Stream stream = mParameters.session.getTrack(i); - if (stream != null) { - String params = mParameters.transport==TRANSPORT_TCP ? - ("TCP;interleaved="+2*i+"-"+(2*i+1)) : ("UDP;unicast;client_port="+(5000+2*i)+"-"+(5000+2*i+1)+";mode=receive"); - String request = "SETUP rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+"/trackID="+i+" RTSP/1.0\r\n" + - "Transport: RTP/AVP/"+params+"\r\n" + - addHeaders(); - - Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); - - mOutputStream.write(request.getBytes("UTF-8")); - mOutputStream.flush(); - Response response = Response.parseResponse(mBufferedReader); - Matcher m; - if (mParameters.transport == TRANSPORT_UDP) { - try { - m = Response.rexegTransport.matcher(response.headers.get("transport")); m.find(); - stream.setDestinationPorts(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4))); - Log.d(TAG, "Setting destination ports: "+Integer.parseInt(m.group(3))+", "+Integer.parseInt(m.group(4))); - } catch (Exception e) { - e.printStackTrace(); - int[] ports = stream.getDestinationPorts(); - Log.d(TAG,"Server did not specify ports, using default ports: "+ports[0]+"-"+ports[1]); - } - } else { - stream.setOutputStream(mOutputStream, (byte)(2*i)); - } - } - } - } - - /** - * Forges and sends the RECORD request - */ - private void sendRequestRecord() throws IllegalStateException, SocketException, IOException { - String request = "RECORD rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + - "Range: npt=0.000-\r\n" + - addHeaders(); - Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); - mOutputStream.write(request.getBytes("UTF-8")); - mOutputStream.flush(); - Response.parseResponse(mBufferedReader); - } - - /** - * Forges and sends the TEARDOWN request - */ - private void sendRequestTeardown() throws IOException { - String request = "TEARDOWN rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + addHeaders(); - Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); - mOutputStream.write(request.getBytes("UTF-8")); - mOutputStream.flush(); - } - - /** - * Forges and sends the OPTIONS request - */ - private void sendRequestOption() throws IOException { - String request = "OPTIONS rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + addHeaders(); - Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); - mOutputStream.write(request.getBytes("UTF-8")); - mOutputStream.flush(); - Response.parseResponse(mBufferedReader); - } - - private String addHeaders() { - return "CSeq: " + (++mCSeq) + "\r\n" + - "Content-Length: 0\r\n" + - "Session: " + mSessionID + "\r\n" + - // For some reason you may have to remove last "\r\n" in the next line to make the RTSP client work with your wowza server :/ - (mAuthorization != null ? "Authorization: " + mAuthorization + "\r\n":"") + "\r\n"; - } - - /** - * If the connection with the RTSP server is lost, we try to reconnect to it as - * long as {@link #stopStream()} is not called. - */ - private Runnable mConnectionMonitor = new Runnable() { - @Override - public void run() { - if (mState == STATE_STARTED) { - try { - // We poll the RTSP server with OPTION requests - sendRequestOption(); - mHandler.postDelayed(mConnectionMonitor, 6000); - } catch (IOException e) { - // Happens if the OPTION request fails - postMessage(ERROR_CONNECTION_LOST); - Log.e(TAG, "Connection lost with the server..."); - mParameters.session.stop(); - mHandler.post(mRetryConnection); - } - } - } - }; - - /** Here, we try to reconnect to the RTSP. */ - private Runnable mRetryConnection = new Runnable() { - @Override - public void run() { - if (mState == STATE_STARTED) { - try { - Log.e(TAG, "Trying to reconnect..."); - tryConnection(); - try { - mParameters.session.start(); - mHandler.post(mConnectionMonitor); - postMessage(MESSAGE_CONNECTION_RECOVERED); - } catch (Exception e) { - abort(); - } - } catch (IOException e) { - mHandler.postDelayed(mRetryConnection,1000); - } - } - } - }; - - final protected static char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; - - private static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - int v; - for ( int j = 0; j < bytes.length; j++ ) { - v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - /** Needed for the Digest Access Authentication. */ - private String computeMd5Hash(String buffer) { - MessageDigest md; - try { - md = MessageDigest.getInstance("MD5"); - return bytesToHex(md.digest(buffer.getBytes("UTF-8"))); - } catch (NoSuchAlgorithmException ignore) { - } catch (UnsupportedEncodingException e) {} - return ""; - } - - private void postMessage(final int message) { - mMainHandler.post(new Runnable() { - @Override - public void run() { - if (mCallback != null) { - mCallback.onRtspUpdate(message, null); - } - } - }); - } - - private void postError(final int message, final Exception e) { - mMainHandler.post(new Runnable() { - @Override - public void run() { - if (mCallback != null) { - mCallback.onRtspUpdate(message, e); - } - } - }); - } - - static class Response { - - // Parses method & uri - public static final Pattern regexStatus = Pattern.compile("RTSP/\\d.\\d (\\d+) (\\w+)",Pattern.CASE_INSENSITIVE); - // Parses a request header - public static final Pattern rexegHeader = Pattern.compile("(\\S+):(.+)",Pattern.CASE_INSENSITIVE); - // Parses a WWW-Authenticate header - public static final Pattern rexegAuthenticate = Pattern.compile("realm=\"(.+)\",\\s+nonce=\"(\\w+)\"",Pattern.CASE_INSENSITIVE); - // Parses a Session header - public static final Pattern rexegSession = Pattern.compile("(\\d+)",Pattern.CASE_INSENSITIVE); - // Parses a Transport header - public static final Pattern rexegTransport = Pattern.compile("client_port=(\\d+)-(\\d+).+server_port=(\\d+)-(\\d+)",Pattern.CASE_INSENSITIVE); - - - public int status; - public HashMap headers = new HashMap(); - - /** Parse the method, URI & headers of a RTSP request */ - public static Response parseResponse(BufferedReader input) throws IOException, IllegalStateException, SocketException { - Response response = new Response(); - String line; - Matcher matcher; - // Parsing request method & URI - if ((line = input.readLine())==null) throw new SocketException("Connection lost"); - matcher = regexStatus.matcher(line); - matcher.find(); - response.status = Integer.parseInt(matcher.group(1)); - - // Parsing headers of the request - while ( (line = input.readLine()) != null) { - //Log.e(TAG,"l: "+line.length()+", c: "+line); - if (line.length()>3) { - matcher = rexegHeader.matcher(line); - matcher.find(); - response.headers.put(matcher.group(1).toLowerCase(Locale.US),matcher.group(2)); - } else { - break; - } - } - if (line==null) throw new SocketException("Connection lost"); - - Log.d(TAG, "Response from server: "+response.status); - - return response; - } - } - -} diff --git a/src/net/majorkernelpanic/streaming/rtsp/RtspServer.java b/src/net/majorkernelpanic/streaming/rtsp/RtspServer.java deleted file mode 100644 index 4696f973..00000000 --- a/src/net/majorkernelpanic/streaming/rtsp/RtspServer.java +++ /dev/null @@ -1,655 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.rtsp; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.BindException; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Locale; -import java.util.WeakHashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import net.majorkernelpanic.streaming.Session; -import net.majorkernelpanic.streaming.SessionBuilder; -import android.app.Service; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.os.Binder; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.util.Log; - -/** - * Implementation of a subset of the RTSP protocol (RFC 2326). - * - * It allows remote control of an android device cameras & microphone. - * For each connected client, a Session is instantiated. - * The Session will start or stop streams according to what the client wants. - * - */ -public class RtspServer extends Service { - - public final static String TAG = "RtspServer"; - - /** The server name that will appear in responses. */ - public static String SERVER_NAME = "MajorKernelPanic RTSP Server"; - - /** Port used by default. */ - public static final int DEFAULT_RTSP_PORT = 8086; - - /** Port already in use. */ - public final static int ERROR_BIND_FAILED = 0x00; - - /** A stream could not be started. */ - public final static int ERROR_START_FAILED = 0x01; - - /** Streaming started. */ - public final static int MESSAGE_STREAMING_STARTED = 0X00; - - /** Streaming stopped. */ - public final static int MESSAGE_STREAMING_STOPPED = 0X01; - - /** Key used in the SharedPreferences to store whether the RTSP server is enabled or not. */ - public final static String KEY_ENABLED = "rtsp_enabled"; - - /** Key used in the SharedPreferences for the port used by the RTSP server. */ - public final static String KEY_PORT = "rtsp_port"; - - protected SessionBuilder mSessionBuilder; - protected SharedPreferences mSharedPreferences; - protected boolean mEnabled = true; - protected int mPort = DEFAULT_RTSP_PORT; - protected WeakHashMap mSessions = new WeakHashMap(2); - - private RequestListener mListenerThread; - private final IBinder mBinder = new LocalBinder(); - private boolean mRestart = false; - private final LinkedList mListeners = new LinkedList(); - - - public RtspServer() { - } - - /** Be careful: those callbacks won't necessarily be called from the ui thread ! */ - public interface CallbackListener { - - /** Called when an error occurs. */ - void onError(RtspServer server, Exception e, int error); - - /** Called when streaming starts/stops. */ - void onMessage(RtspServer server, int message); - - } - - /** - * See {@link RtspServer.CallbackListener} to check out what events will be fired once you set up a listener. - * @param listener The listener - */ - public void addCallbackListener(CallbackListener listener) { - synchronized (mListeners) { - if (mListeners.size() > 0) { - for (CallbackListener cl : mListeners) { - if (cl == listener) return; - } - } - mListeners.add(listener); - } - } - - /** - * Removes the listener. - * @param listener The listener - */ - public void removeCallbackListener(CallbackListener listener) { - synchronized (mListeners) { - mListeners.remove(listener); - } - } - - /** Returns the port used by the RTSP server. */ - public int getPort() { - return mPort; - } - - /** - * Sets the port for the RTSP server to use. - * @param port The port - */ - public void setPort(int port) { - Editor editor = mSharedPreferences.edit(); - editor.putString(KEY_PORT, String.valueOf(port)); - editor.commit(); - } - - /** - * Starts (or restart if needed, if for example the configuration - * of the server has been modified) the RTSP server. - */ - public void start() { - if (!mEnabled || mRestart) stop(); - if (mEnabled && mListenerThread == null) { - try { - mListenerThread = new RequestListener(); - } catch (Exception e) { - mListenerThread = null; - } - } - mRestart = false; - } - - /** - * Stops the RTSP server but not the Android Service. - * To stop the Android Service you need to call {@link android.content.Context#stopService(Intent)}; - */ - public void stop() { - if (mListenerThread != null) { - try { - mListenerThread.kill(); - for ( Session session : mSessions.keySet() ) { - if ( session != null ) { - if (session.isStreaming()) session.stop(); - } - } - } catch (Exception e) { - } finally { - mListenerThread = null; - } - } - } - - /** Returns whether or not the RTSP server is streaming to some client(s). */ - public boolean isStreaming() { - for ( Session session : mSessions.keySet() ) { - if ( session != null ) { - if (session.isStreaming()) return true; - } - } - return false; - } - - public boolean isEnabled() { - return mEnabled; - } - - /** Returns the bandwidth consumed by the RTSP server in bits per second. */ - public long getBitrate() { - long bitrate = 0; - for ( Session session : mSessions.keySet() ) { - if ( session != null ) { - if (session.isStreaming()) bitrate += session.getBitrate(); - } - } - return bitrate; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return START_STICKY; - } - - @Override - public void onCreate() { - - // Let's restore the state of the service - mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - mPort = Integer.parseInt(mSharedPreferences.getString(KEY_PORT, String.valueOf(mPort))); - mEnabled = mSharedPreferences.getBoolean(KEY_ENABLED, mEnabled); - - // If the configuration is modified, the server will adjust - mSharedPreferences.registerOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener); - - start(); - } - - @Override - public void onDestroy() { - stop(); - mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener); - } - - private OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - - if (key.equals(KEY_PORT)) { - int port = Integer.parseInt(sharedPreferences.getString(KEY_PORT, String.valueOf(mPort))); - if (port != mPort) { - mPort = port; - mRestart = true; - start(); - } - } - else if (key.equals(KEY_ENABLED)) { - mEnabled = sharedPreferences.getBoolean(KEY_ENABLED, mEnabled); - start(); - } - } - }; - - /** The Binder you obtain when a connection with the Service is established. */ - public class LocalBinder extends Binder { - public RtspServer getService() { - return RtspServer.this; - } - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - protected void postMessage(int id) { - synchronized (mListeners) { - if (mListeners.size() > 0) { - for (CallbackListener cl : mListeners) { - cl.onMessage(this, id); - } - } - } - } - - protected void postError(Exception exception, int id) { - synchronized (mListeners) { - if (mListeners.size() > 0) { - for (CallbackListener cl : mListeners) { - cl.onError(this, exception, id); - } - } - } - } - - /** - * By default the RTSP uses {@link UriParser} to parse the URI requested by the client - * but you can change that behavior by override this method. - * @param uri The uri that the client has requested - * @param client The socket associated to the client - * @return A proper session - */ - protected Session handleRequest(String uri, Socket client) throws IllegalStateException, IOException { - Session session = UriParser.parse(uri); - session.setOrigin(client.getLocalAddress().getHostAddress()); - if (session.getDestination()==null) { - session.setDestination(client.getInetAddress().getHostAddress()); - } - return session; - } - - class RequestListener extends Thread implements Runnable { - - private final ServerSocket mServer; - - public RequestListener() throws IOException { - try { - mServer = new ServerSocket(mPort); - start(); - } catch (BindException e) { - Log.e(TAG,"Port already in use !"); - postError(e, ERROR_BIND_FAILED); - throw e; - } - } - - public void run() { - Log.i(TAG,"RTSP server listening on port "+mServer.getLocalPort()); - while (!Thread.interrupted()) { - try { - new WorkerThread(mServer.accept()).start(); - } catch (SocketException e) { - break; - } catch (IOException e) { - Log.e(TAG,e.getMessage()); - continue; - } - } - Log.i(TAG,"RTSP server stopped !"); - } - - public void kill() { - try { - mServer.close(); - } catch (IOException e) {} - try { - this.join(); - } catch (InterruptedException ignore) {} - } - - } - - // One thread per client - class WorkerThread extends Thread implements Runnable { - - private final Socket mClient; - private final OutputStream mOutput; - private final BufferedReader mInput; - - // Each client has an associated session - private Session mSession; - - public WorkerThread(final Socket client) throws IOException { - mInput = new BufferedReader(new InputStreamReader(client.getInputStream())); - mOutput = client.getOutputStream(); - mClient = client; - mSession = new Session(); - } - - public void run() { - Request request; - Response response; - - Log.i(TAG, "Connection from "+mClient.getInetAddress().getHostAddress()); - - while (!Thread.interrupted()) { - - request = null; - response = null; - - // Parse the request - try { - request = Request.parseRequest(mInput); - } catch (SocketException e) { - // Client has left - break; - } catch (Exception e) { - // We don't understand the request :/ - response = new Response(); - response.status = Response.STATUS_BAD_REQUEST; - } - - // Do something accordingly like starting the streams, sending a session description - if (request != null) { - try { - response = processRequest(request); - } - catch (Exception e) { - // This alerts the main thread that something has gone wrong in this thread - postError(e, ERROR_START_FAILED); - Log.e(TAG,e.getMessage()!=null?e.getMessage():"An error occurred"); - e.printStackTrace(); - response = new Response(request); - } - } - - // We always send a response - // The client will receive an "INTERNAL SERVER ERROR" if an exception has been thrown at some point - try { - response.send(mOutput); - } catch (IOException e) { - Log.e(TAG,"Response was not sent properly"); - break; - } - - } - - // Streaming stops when client disconnects - boolean streaming = isStreaming(); - mSession.syncStop(); - if (streaming && !isStreaming()) { - postMessage(MESSAGE_STREAMING_STOPPED); - } - mSession.release(); - - try { - mClient.close(); - } catch (IOException ignore) {} - - Log.i(TAG, "Client disconnected"); - - } - - public Response processRequest(Request request) throws IllegalStateException, IOException { - Response response = new Response(request); - - /* ********************************************************************************** */ - /* ********************************* Method DESCRIBE ******************************** */ - /* ********************************************************************************** */ - if (request.method.equalsIgnoreCase("DESCRIBE")) { - - // Parse the requested URI and configure the session - mSession = handleRequest(request.uri, mClient); - mSessions.put(mSession, null); - mSession.syncConfigure(); - - String requestContent = mSession.getSessionDescription(); - String requestAttributes = - "Content-Base: "+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/\r\n" + - "Content-Type: application/sdp\r\n"; - - response.attributes = requestAttributes; - response.content = requestContent; - - // If no exception has been thrown, we reply with OK - response.status = Response.STATUS_OK; - - } - - /* ********************************************************************************** */ - /* ********************************* Method OPTIONS ********************************* */ - /* ********************************************************************************** */ - else if (request.method.equalsIgnoreCase("OPTIONS")) { - response.status = Response.STATUS_OK; - response.attributes = "Public: DESCRIBE,SETUP,TEARDOWN,PLAY,PAUSE\r\n"; - response.status = Response.STATUS_OK; - } - - /* ********************************************************************************** */ - /* ********************************** Method SETUP ********************************** */ - /* ********************************************************************************** */ - else if (request.method.equalsIgnoreCase("SETUP")) { - Pattern p; Matcher m; - int p2, p1, ssrc, trackId, src[]; - String destination; - - p = Pattern.compile("trackID=(\\w+)",Pattern.CASE_INSENSITIVE); - m = p.matcher(request.uri); - - if (!m.find()) { - response.status = Response.STATUS_BAD_REQUEST; - return response; - } - - trackId = Integer.parseInt(m.group(1)); - - if (!mSession.trackExists(trackId)) { - response.status = Response.STATUS_NOT_FOUND; - return response; - } - - p = Pattern.compile("client_port=(\\d+)-(\\d+)",Pattern.CASE_INSENSITIVE); - m = p.matcher(request.headers.get("transport")); - - if (!m.find()) { - int[] ports = mSession.getTrack(trackId).getDestinationPorts(); - p1 = ports[0]; - p2 = ports[1]; - } - else { - p1 = Integer.parseInt(m.group(1)); - p2 = Integer.parseInt(m.group(2)); - } - - ssrc = mSession.getTrack(trackId).getSSRC(); - src = mSession.getTrack(trackId).getLocalPorts(); - destination = mSession.getDestination(); - - mSession.getTrack(trackId).setDestinationPorts(p1, p2); - - boolean streaming = isStreaming(); - mSession.syncStart(trackId); - if (!streaming && isStreaming()) { - postMessage(MESSAGE_STREAMING_STARTED); - } - - response.attributes = "Transport: RTP/AVP/UDP;"+(InetAddress.getByName(destination).isMulticastAddress()?"multicast":"unicast")+ - ";destination="+mSession.getDestination()+ - ";client_port="+p1+"-"+p2+ - ";server_port="+src[0]+"-"+src[1]+ - ";ssrc="+Integer.toHexString(ssrc)+ - ";mode=play\r\n" + - "Session: "+ "1185d20035702ca" + "\r\n" + - "Cache-Control: no-cache\r\n"; - response.status = Response.STATUS_OK; - - // If no exception has been thrown, we reply with OK - response.status = Response.STATUS_OK; - - } - - /* ********************************************************************************** */ - /* ********************************** Method PLAY *********************************** */ - /* ********************************************************************************** */ - else if (request.method.equalsIgnoreCase("PLAY")) { - String requestAttributes = "RTP-Info: "; - if (mSession.trackExists(0)) requestAttributes += "url=rtsp://"+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/trackID="+0+";seq=0,"; - if (mSession.trackExists(1)) requestAttributes += "url=rtsp://"+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/trackID="+1+";seq=0,"; - requestAttributes = requestAttributes.substring(0, requestAttributes.length()-1) + "\r\nSession: 1185d20035702ca\r\n"; - - response.attributes = requestAttributes; - - // If no exception has been thrown, we reply with OK - response.status = Response.STATUS_OK; - - } - - /* ********************************************************************************** */ - /* ********************************** Method PAUSE ********************************** */ - /* ********************************************************************************** */ - else if (request.method.equalsIgnoreCase("PAUSE")) { - response.status = Response.STATUS_OK; - } - - /* ********************************************************************************** */ - /* ********************************* Method TEARDOWN ******************************** */ - /* ********************************************************************************** */ - else if (request.method.equalsIgnoreCase("TEARDOWN")) { - response.status = Response.STATUS_OK; - } - - /* ********************************************************************************** */ - /* ********************************* Unknown method ? ******************************* */ - /* ********************************************************************************** */ - else { - Log.e(TAG,"Command unknown: "+request); - response.status = Response.STATUS_BAD_REQUEST; - } - - return response; - - } - - } - - static class Request { - - // Parse method & uri - public static final Pattern regexMethod = Pattern.compile("(\\w+) (\\S+) RTSP",Pattern.CASE_INSENSITIVE); - // Parse a request header - public static final Pattern rexegHeader = Pattern.compile("(\\S+):(.+)",Pattern.CASE_INSENSITIVE); - - public String method; - public String uri; - public HashMap headers = new HashMap(); - - /** Parse the method, uri & headers of a RTSP request */ - public static Request parseRequest(BufferedReader input) throws IOException, IllegalStateException, SocketException { - Request request = new Request(); - String line; - Matcher matcher; - - // Parsing request method & uri - if ((line = input.readLine())==null) throw new SocketException("Client disconnected"); - matcher = regexMethod.matcher(line); - matcher.find(); - request.method = matcher.group(1); - request.uri = matcher.group(2); - - // Parsing headers of the request - while ( (line = input.readLine()) != null && line.length()>3 ) { - matcher = rexegHeader.matcher(line); - matcher.find(); - request.headers.put(matcher.group(1).toLowerCase(Locale.US),matcher.group(2)); - } - if (line==null) throw new SocketException("Client disconnected"); - - // It's not an error, it's just easier to follow what's happening in logcat with the request in red - Log.e(TAG,request.method+" "+request.uri); - - return request; - } - } - - static class Response { - - // Status code definitions - public static final String STATUS_OK = "200 OK"; - public static final String STATUS_BAD_REQUEST = "400 Bad Request"; - public static final String STATUS_NOT_FOUND = "404 Not Found"; - public static final String STATUS_INTERNAL_SERVER_ERROR = "500 Internal Server Error"; - - public String status = STATUS_INTERNAL_SERVER_ERROR; - public String content = ""; - public String attributes = ""; - - private final Request mRequest; - - public Response(Request request) { - this.mRequest = request; - } - - public Response() { - // Be carefull if you modify the send() method because request might be null ! - mRequest = null; - } - - public void send(OutputStream output) throws IOException { - int seqid = -1; - - try { - seqid = Integer.parseInt(mRequest.headers.get("cseq").replace(" ","")); - } catch (Exception e) { - Log.e(TAG,"Error parsing CSeq: "+(e.getMessage()!=null?e.getMessage():"")); - } - - String response = "RTSP/1.0 "+status+"\r\n" + - "Server: "+SERVER_NAME+"\r\n" + - (seqid>=0?("Cseq: " + seqid + "\r\n"):"") + - "Content-Length: " + content.length() + "\r\n" + - attributes + - "\r\n" + - content; - - Log.d(TAG,response.replace("\r", "")); - - output.write(response.getBytes()); - } - } - -} diff --git a/src/net/majorkernelpanic/streaming/rtsp/UriParser.java b/src/net/majorkernelpanic/streaming/rtsp/UriParser.java deleted file mode 100644 index 23535286..00000000 --- a/src/net/majorkernelpanic/streaming/rtsp/UriParser.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.rtsp; - -import static net.majorkernelpanic.streaming.SessionBuilder.AUDIO_AAC; -import static net.majorkernelpanic.streaming.SessionBuilder.AUDIO_AMRNB; -import static net.majorkernelpanic.streaming.SessionBuilder.AUDIO_NONE; -import static net.majorkernelpanic.streaming.SessionBuilder.VIDEO_H263; -import static net.majorkernelpanic.streaming.SessionBuilder.VIDEO_H264; -import static net.majorkernelpanic.streaming.SessionBuilder.VIDEO_NONE; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.URI; -import java.net.UnknownHostException; -import java.util.Iterator; -import java.util.List; - -import net.majorkernelpanic.streaming.MediaStream; -import net.majorkernelpanic.streaming.Session; -import net.majorkernelpanic.streaming.SessionBuilder; -import net.majorkernelpanic.streaming.audio.AudioQuality; -import net.majorkernelpanic.streaming.video.VideoQuality; - -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; - -import android.hardware.Camera.CameraInfo; - -/** - * This class parses URIs received by the RTSP server and configures a Session accordingly. - */ -public class UriParser { - - public final static String TAG = "UriParser"; - - /** - * Configures a Session according to the given URI. - * Here are some examples of URIs that can be used to configure a Session: - *
  • rtsp://xxx.xxx.xxx.xxx:8086?h264&flash=on
  • - *
  • rtsp://xxx.xxx.xxx.xxx:8086?h263&camera=front&flash=on
  • - *
  • rtsp://xxx.xxx.xxx.xxx:8086?h264=200-20-320-240
  • - *
  • rtsp://xxx.xxx.xxx.xxx:8086?aac
- * @param uri The URI - * @throws IllegalStateException - * @throws IOException - * @return A Session configured according to the URI - */ - public static Session parse(String uri) throws IllegalStateException, IOException { - SessionBuilder builder = SessionBuilder.getInstance().clone(); - byte audioApi = 0, videoApi = 0; - - List params = URLEncodedUtils.parse(URI.create(uri),"UTF-8"); - if (params.size()>0) { - - builder.setAudioEncoder(AUDIO_NONE).setVideoEncoder(VIDEO_NONE); - - // Those parameters must be parsed first or else they won't necessarily be taken into account - for (Iterator it = params.iterator();it.hasNext();) { - NameValuePair param = it.next(); - - // FLASH ON/OFF - if (param.getName().equalsIgnoreCase("flash")) { - if (param.getValue().equalsIgnoreCase("on")) - builder.setFlashEnabled(true); - else - builder.setFlashEnabled(false); - } - - // CAMERA -> the client can choose between the front facing camera and the back facing camera - else if (param.getName().equalsIgnoreCase("camera")) { - if (param.getValue().equalsIgnoreCase("back")) - builder.setCamera(CameraInfo.CAMERA_FACING_BACK); - else if (param.getValue().equalsIgnoreCase("front")) - builder.setCamera(CameraInfo.CAMERA_FACING_FRONT); - } - - // MULTICAST -> the stream will be sent to a multicast group - // The default mutlicast address is 228.5.6.7, but the client can specify another - else if (param.getName().equalsIgnoreCase("multicast")) { - if (param.getValue()!=null) { - try { - InetAddress addr = InetAddress.getByName(param.getValue()); - if (!addr.isMulticastAddress()) { - throw new IllegalStateException("Invalid multicast address !"); - } - builder.setDestination(param.getValue()); - } catch (UnknownHostException e) { - throw new IllegalStateException("Invalid multicast address !"); - } - } - else { - // Default multicast address - builder.setDestination("228.5.6.7"); - } - } - - // UNICAST -> the client can use this to specify where he wants the stream to be sent - else if (param.getName().equalsIgnoreCase("unicast")) { - if (param.getValue()!=null) { - builder.setDestination(param.getValue()); - } - } - - // VIDEOAPI -> can be used to specify what api will be used to encode video (the MediaRecorder API or the MediaCodec API) - else if (param.getName().equalsIgnoreCase("videoapi")) { - if (param.getValue()!=null) { - if (param.getValue().equalsIgnoreCase("mr")) { - videoApi = MediaStream.MODE_MEDIARECORDER_API; - } else if (param.getValue().equalsIgnoreCase("mc")) { - videoApi = MediaStream.MODE_MEDIACODEC_API; - } - } - } - - // AUDIOAPI -> can be used to specify what api will be used to encode audio (the MediaRecorder API or the MediaCodec API) - else if (param.getName().equalsIgnoreCase("audioapi")) { - if (param.getValue()!=null) { - if (param.getValue().equalsIgnoreCase("mr")) { - audioApi = MediaStream.MODE_MEDIARECORDER_API; - } else if (param.getValue().equalsIgnoreCase("mc")) { - audioApi = MediaStream.MODE_MEDIACODEC_API; - } - } - } - - // TTL -> the client can modify the time to live of packets - // By default ttl=64 - else if (param.getName().equalsIgnoreCase("ttl")) { - if (param.getValue()!=null) { - try { - int ttl = Integer.parseInt(param.getValue()); - if (ttl<0) throw new IllegalStateException(); - builder.setTimeToLive(ttl); - } catch (Exception e) { - throw new IllegalStateException("The TTL must be a positive integer !"); - } - } - } - - // H.264 - else if (param.getName().equalsIgnoreCase("h264")) { - VideoQuality quality = VideoQuality.parseQuality(param.getValue()); - builder.setVideoQuality(quality).setVideoEncoder(VIDEO_H264); - } - - // H.263 - else if (param.getName().equalsIgnoreCase("h263")) { - VideoQuality quality = VideoQuality.parseQuality(param.getValue()); - builder.setVideoQuality(quality).setVideoEncoder(VIDEO_H263); - } - - // AMR - else if (param.getName().equalsIgnoreCase("amrnb") || param.getName().equalsIgnoreCase("amr")) { - AudioQuality quality = AudioQuality.parseQuality(param.getValue()); - builder.setAudioQuality(quality).setAudioEncoder(AUDIO_AMRNB); - } - - // AAC - else if (param.getName().equalsIgnoreCase("aac")) { - AudioQuality quality = AudioQuality.parseQuality(param.getValue()); - builder.setAudioQuality(quality).setAudioEncoder(AUDIO_AAC); - } - - } - - } - - if (builder.getVideoEncoder()==VIDEO_NONE && builder.getAudioEncoder()==AUDIO_NONE) { - SessionBuilder b = SessionBuilder.getInstance(); - builder.setVideoEncoder(b.getVideoEncoder()); - builder.setAudioEncoder(b.getAudioEncoder()); - } - - Session session = builder.build(); - - if (videoApi>0 && session.getVideoTrack() != null) { - session.getVideoTrack().setStreamingMethod(videoApi); - } - - if (audioApi>0 && session.getAudioTrack() != null) { - session.getAudioTrack().setStreamingMethod(audioApi); - } - - return session; - - } - -} diff --git a/src/net/majorkernelpanic/streaming/video/CodecManager.java b/src/net/majorkernelpanic/streaming/video/CodecManager.java deleted file mode 100644 index 0d9d4cc8..00000000 --- a/src/net/majorkernelpanic/streaming/video/CodecManager.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright (C) 2011-2013 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.video; - -import java.util.ArrayList; -import java.util.HashMap; - -import android.annotation.SuppressLint; -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.os.Build; -import android.util.Log; -import android.util.SparseArray; - -@SuppressLint("InlinedApi") -public class CodecManager { - - public final static String TAG = "CodecManager"; - - public static final int[] SUPPORTED_COLOR_FORMATS = { - MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar, - MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar - }; - - /** - * There currently is no way to know if an encoder is software or hardware from the MediaCodecInfo class, - * so we need to maintain a list of known software encoders. - */ - public static final String[] SOFTWARE_ENCODERS = { - "OMX.google.h264.encoder" - }; - - /** - * Contains a list of encoders and color formats that we may use with a {@link CodecManager.Translator}. - */ - static class Codecs { - /** A hardware encoder supporting a color format we can use. */ - public String hardwareCodec; - public int hardwareColorFormat; - /** A software encoder supporting a color format we can use. */ - public String softwareCodec; - public int softwareColorFormat; - } - - /** - * Contains helper functions to choose an encoder and a color format. - */ - static class Selector { - - private static HashMap>> sHardwareCodecs = new HashMap>>(); - private static HashMap>> sSoftwareCodecs = new HashMap>>(); - - /** - * Determines the most appropriate encoder to compress the video from the Camera - */ - public static Codecs findCodecsFormMimeType(String mimeType, boolean tryColorFormatSurface) { - findSupportedColorFormats(mimeType); - SparseArray> hardwareCodecs = sHardwareCodecs.get(mimeType); - SparseArray> softwareCodecs = sSoftwareCodecs.get(mimeType); - Codecs list = new Codecs(); - - // On devices running 4.3, we need an encoder supporting the color format used to work with a Surface - if (Build.VERSION.SDK_INT>=18 && tryColorFormatSurface) { - int colorFormatSurface = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; - try { - // We want a hardware encoder - list.hardwareCodec = hardwareCodecs.get(colorFormatSurface).get(0); - list.hardwareColorFormat = colorFormatSurface; - } catch (Exception e) {} - try { - // We want a software encoder - list.softwareCodec = softwareCodecs.get(colorFormatSurface).get(0); - list.softwareColorFormat = colorFormatSurface; - } catch (Exception e) {} - - if (list.hardwareCodec != null) { - Log.v(TAG,"Choosen primary codec: "+list.hardwareCodec+" with color format: "+list.hardwareColorFormat); - } else { - Log.e(TAG,"No supported hardware codec found !"); - } - if (list.softwareCodec != null) { - Log.v(TAG,"Choosen secondary codec: "+list.hardwareCodec+" with color format: "+list.hardwareColorFormat); - } else { - Log.e(TAG,"No supported software codec found !"); - } - return list; - } - - for (int i=0;i> softwareCodecs = new SparseArray>(); - SparseArray> hardwareCodecs = new SparseArray>(); - - if (sSoftwareCodecs.containsKey(mimeType)) { - return; - } - - Log.v(TAG,"Searching supported color formats for mime type \""+mimeType+"\"..."); - - // We loop through the encoders, apparently this can take up to a sec (testes on a GS3) - for(int j = MediaCodecList.getCodecCount() - 1; j >= 0; j--){ - MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(j); - if (!codecInfo.isEncoder()) continue; - - String[] types = codecInfo.getSupportedTypes(); - for (int i = 0; i < types.length; i++) { - if (types[i].equalsIgnoreCase(mimeType)) { - MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType); - - boolean software = false; - for (int k=0;k()); - softwareCodecs.get(format).add(codecInfo.getName()); - } else { - if (hardwareCodecs.get(format) == null) hardwareCodecs.put(format, new ArrayList()); - hardwareCodecs.get(format).add(codecInfo.getName()); - } - } - - } - } - } - - // Logs the supported color formats on the phone - StringBuilder e = new StringBuilder(); - e.append("Supported color formats on this phone: "); - for (int i=0;i=640) { - // Using the MediaCodec API with the buffer method for high resolutions is too slow - mMode = MODE_MEDIARECORDER_API; - } - EncoderDebugger debugger = EncoderDebugger.debug(mSettings, mQuality.resX, mQuality.resY); - return new MP4Config(debugger.getB64SPS(), debugger.getB64PPS()); - } catch (Exception e) { - // Fallback on the old streaming method using the MediaRecorder API - Log.e(TAG,"Resolution not supported with the MediaCodec API, we fallback on the old streamign method."); - mMode = MODE_MEDIARECORDER_API; - return testH264(); - } - } - - // Should not be called by the UI thread - private MP4Config testMediaRecorderAPI() throws RuntimeException, IOException { - String key = PREF_PREFIX+"h264-mr-"+mRequestedQuality.framerate+","+mRequestedQuality.resX+","+mRequestedQuality.resY; - - if (mSettings != null) { - if (mSettings.contains(key)) { - String[] s = mSettings.getString(key, "").split(","); - return new MP4Config(s[0],s[1],s[2]); - } - } - - if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - throw new StorageUnavailableException("No external storage or external storage not ready !"); - } - - final String TESTFILE = Environment.getExternalStorageDirectory().getPath()+"/spydroid-test.mp4"; - - Log.i(TAG,"Testing H264 support... Test file saved at: "+TESTFILE); - - try { - File file = new File(TESTFILE); - file.createNewFile(); - } catch (IOException e) { - throw new StorageUnavailableException(e.getMessage()); - } - - // Save flash state & set it to false so that led remains off while testing h264 - boolean savedFlashState = mFlashEnabled; - mFlashEnabled = false; - - boolean previewStarted = mPreviewStarted; - - boolean cameraOpen = mCamera!=null; - createCamera(); - - // Stops the preview if needed - if (mPreviewStarted) { - lockCamera(); - try { - mCamera.stopPreview(); - } catch (Exception e) {} - mPreviewStarted = false; - } - - try { - Thread.sleep(100); - } catch (InterruptedException e1) { - // TODO Auto-generated catch block - e1.printStackTrace(); - } - - unlockCamera(); - - try { - - mMediaRecorder = new MediaRecorder(); - mMediaRecorder.setCamera(mCamera); - mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); - mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); - mMediaRecorder.setVideoEncoder(mVideoEncoder); - mMediaRecorder.setPreviewDisplay(mSurfaceView.getHolder().getSurface()); - mMediaRecorder.setVideoSize(mRequestedQuality.resX,mRequestedQuality.resY); - mMediaRecorder.setVideoFrameRate(mRequestedQuality.framerate); - mMediaRecorder.setVideoEncodingBitRate((int)(mRequestedQuality.bitrate*0.8)); - mMediaRecorder.setOutputFile(TESTFILE); - mMediaRecorder.setMaxDuration(3000); - - // We wait a little and stop recording - mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { - public void onInfo(MediaRecorder mr, int what, int extra) { - Log.d(TAG,"MediaRecorder callback called !"); - if (what==MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) { - Log.d(TAG,"MediaRecorder: MAX_DURATION_REACHED"); - } else if (what==MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { - Log.d(TAG,"MediaRecorder: MAX_FILESIZE_REACHED"); - } else if (what==MediaRecorder.MEDIA_RECORDER_INFO_UNKNOWN) { - Log.d(TAG,"MediaRecorder: INFO_UNKNOWN"); - } else { - Log.d(TAG,"WTF ?"); - } - mLock.release(); - } - }); - - // Start recording - mMediaRecorder.prepare(); - mMediaRecorder.start(); - - if (mLock.tryAcquire(6,TimeUnit.SECONDS)) { - Log.d(TAG,"MediaRecorder callback was called :)"); - Thread.sleep(400); - } else { - Log.d(TAG,"MediaRecorder callback was not called after 6 seconds... :("); - } - } catch (IOException e) { - throw new ConfNotSupportedException(e.getMessage()); - } catch (RuntimeException e) { - throw new ConfNotSupportedException(e.getMessage()); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - try { - mMediaRecorder.stop(); - } catch (Exception e) {} - mMediaRecorder.release(); - mMediaRecorder = null; - lockCamera(); - if (!cameraOpen) destroyCamera(); - // Restore flash state - mFlashEnabled = savedFlashState; - if (previewStarted) { - // If the preview was started before the test, we try to restart it. - try { - startPreview(); - } catch (Exception e) {} - } - } - - // Retrieve SPS & PPS & ProfileId with MP4Config - MP4Config config = new MP4Config(TESTFILE); - - // Delete dummy video - File file = new File(TESTFILE); - if (!file.delete()) Log.e(TAG,"Temp file could not be erased"); - - Log.i(TAG,"H264 Test succeded..."); - - // Save test result - if (mSettings != null) { - Editor editor = mSettings.edit(); - editor.putString(key, config.getProfileLevel()+","+config.getB64SPS()+","+config.getB64PPS()); - editor.commit(); - } - - return config; - - } - -} diff --git a/src/net/majorkernelpanic/streaming/video/VideoQuality.java b/src/net/majorkernelpanic/streaming/video/VideoQuality.java deleted file mode 100644 index 8c857f86..00000000 --- a/src/net/majorkernelpanic/streaming/video/VideoQuality.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.video; - -import java.util.Iterator; -import java.util.List; - -import android.hardware.Camera; -import android.hardware.Camera.Size; -import android.util.Log; - -/** - * A class that represents the quality of a video stream. - * It contains the resolution, the framerate (in fps) and the bitrate (in bps) of the stream. - */ -public class VideoQuality { - - public final static String TAG = "VideoQuality"; - - /** Default video stream quality. */ - public final static VideoQuality DEFAULT_VIDEO_QUALITY = new VideoQuality(176,144,20,500000); - - /** Represents a quality for a video stream. */ - public VideoQuality() {} - - /** - * Represents a quality for a video stream. - * @param resX The horizontal resolution - * @param resY The vertical resolution - */ - public VideoQuality(int resX, int resY) { - this.resX = resX; - this.resY = resY; - } - - /** - * Represents a quality for a video stream. - * @param resX The horizontal resolution - * @param resY The vertical resolution - * @param framerate The framerate in frame per seconds - * @param bitrate The bitrate in bit per seconds - */ - public VideoQuality(int resX, int resY, int framerate, int bitrate) { - this.framerate = framerate; - this.bitrate = bitrate; - this.resX = resX; - this.resY = resY; - } - - public int framerate = 0; - public int bitrate = 0; - public int resX = 0; - public int resY = 0; - - public boolean equals(VideoQuality quality) { - if (quality==null) return false; - return (quality.resX == this.resX & - quality.resY == this.resY & - quality.framerate == this.framerate & - quality.bitrate == this.bitrate); - } - - public VideoQuality clone() { - return new VideoQuality(resX,resY,framerate,bitrate); - } - - public static VideoQuality parseQuality(String str) { - VideoQuality quality = DEFAULT_VIDEO_QUALITY.clone(); - if (str != null) { - String[] config = str.split("-"); - try { - quality.bitrate = Integer.parseInt(config[0])*1000; // conversion to bit/s - quality.framerate = Integer.parseInt(config[1]); - quality.resX = Integer.parseInt(config[2]); - quality.resY = Integer.parseInt(config[3]); - } - catch (IndexOutOfBoundsException ignore) {} - } - return quality; - } - - public String toString() { - return resX+"x"+resY+" px, "+framerate+" fps, "+bitrate/1000+" kbps"; - } - - /** - * Checks if the requested resolution is supported by the camera. - * If not, it modifies it by supported parameters. - **/ - public static VideoQuality determineClosestSupportedResolution(Camera.Parameters parameters, VideoQuality quality) { - VideoQuality v = quality.clone(); - int minDist = Integer.MAX_VALUE; - String supportedSizesStr = "Supported resolutions: "; - List supportedSizes = parameters.getSupportedPreviewSizes(); - for (Iterator it = supportedSizes.iterator(); it.hasNext();) { - Size size = it.next(); - supportedSizesStr += size.width+"x"+size.height+(it.hasNext()?", ":""); - int dist = Math.abs(quality.resX - size.width); - if (dist"+v.resX+"x"+v.resY); - } - - return v; - } - - public static int[] determineMaximumSupportedFramerate(Camera.Parameters parameters) { - int[] maxFps = new int[]{0,0}; - String supportedFpsRangesStr = "Supported frame rates: "; - List supportedFpsRanges = parameters.getSupportedPreviewFpsRange(); - for (Iterator it = supportedFpsRanges.iterator(); it.hasNext();) { - int[] interval = it.next(); - // Intervals are returned as integers, for example "29970" means "29.970" FPS. - supportedFpsRangesStr += interval[0]/1000+"-"+interval[1]/1000+"fps"+(it.hasNext()?", ":""); - if (interval[1]>maxFps[1] || (interval[0]>maxFps[0] && interval[1]==maxFps[1])) { - maxFps = interval; - } - } - Log.v(TAG,supportedFpsRangesStr); - return maxFps; - } - -} diff --git a/src/net/majorkernelpanic/streaming/video/VideoStream.java b/src/net/majorkernelpanic/streaming/video/VideoStream.java deleted file mode 100644 index 165fb8db..00000000 --- a/src/net/majorkernelpanic/streaming/video/VideoStream.java +++ /dev/null @@ -1,729 +0,0 @@ -/* - * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com - * - * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) - * - * Spydroid is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -package net.majorkernelpanic.streaming.video; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; - -import net.majorkernelpanic.streaming.MediaStream; -import net.majorkernelpanic.streaming.Stream; -import net.majorkernelpanic.streaming.exceptions.CameraInUseException; -import net.majorkernelpanic.streaming.exceptions.ConfNotSupportedException; -import net.majorkernelpanic.streaming.exceptions.InvalidSurfaceException; -import net.majorkernelpanic.streaming.gl.SurfaceView; -import net.majorkernelpanic.streaming.hw.EncoderDebugger; -import net.majorkernelpanic.streaming.hw.NV21Convertor; -import net.majorkernelpanic.streaming.rtp.MediaCodecInputStream; -import android.annotation.SuppressLint; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.hardware.Camera; -import android.hardware.Camera.CameraInfo; -import android.hardware.Camera.Parameters; -import android.media.MediaCodec; -import android.media.MediaCodecInfo; -import android.media.MediaFormat; -import android.media.MediaRecorder; -import android.os.Looper; -import android.util.Log; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.SurfaceHolder.Callback; - -/** - * Don't use this class directly. - */ -public abstract class VideoStream extends MediaStream { - - protected final static String TAG = "VideoStream"; - - protected VideoQuality mRequestedQuality = VideoQuality.DEFAULT_VIDEO_QUALITY.clone(); - protected VideoQuality mQuality = mRequestedQuality.clone(); - protected SurfaceHolder.Callback mSurfaceHolderCallback = null; - protected SurfaceView mSurfaceView = null; - protected SharedPreferences mSettings = null; - protected int mVideoEncoder, mCameraId = 0; - protected int mRequestedOrientation = 0, mOrientation = 0; - protected Camera mCamera; - protected Thread mCameraThread; - protected Looper mCameraLooper; - - protected boolean mCameraOpenedManually = true; - protected boolean mFlashEnabled = false; - protected boolean mSurfaceReady = false; - protected boolean mUnlocked = false; - protected boolean mPreviewStarted = false; - protected boolean mUpdated = false; - - protected String mMimeType; - protected String mEncoderName; - protected int mEncoderColorFormat; - protected int mCameraImageFormat; - protected int mMaxFps = 0; - - /** - * Don't use this class directly. - * Uses CAMERA_FACING_BACK by default. - */ - public VideoStream() { - this(CameraInfo.CAMERA_FACING_BACK); - } - - /** - * Don't use this class directly - * @param camera Can be either CameraInfo.CAMERA_FACING_BACK or CameraInfo.CAMERA_FACING_FRONT - */ - @SuppressLint("InlinedApi") - public VideoStream(int camera) { - super(); - setCamera(camera); - } - - /** - * Sets the camera that will be used to capture video. - * You can call this method at any time and changes will take effect next time you start the stream. - * @param camera Can be either CameraInfo.CAMERA_FACING_BACK or CameraInfo.CAMERA_FACING_FRONT - */ - public void setCamera(int camera) { - CameraInfo cameraInfo = new CameraInfo(); - int numberOfCameras = Camera.getNumberOfCameras(); - for (int i=0;i3) { - i = 0; - //Log.d(TAG,"Measured: "+1000000L/(now-oldnow)+" fps."); - } - try { - int bufferIndex = mMediaCodec.dequeueInputBuffer(500000); - if (bufferIndex>=0) { - inputBuffers[bufferIndex].clear(); - if (data == null) Log.e(TAG,"Symptom of the \"Callback buffer was to small\" problem..."); - else convertor.convert(data, inputBuffers[bufferIndex]); - mMediaCodec.queueInputBuffer(bufferIndex, 0, inputBuffers[bufferIndex].position(), now, 0); - } else { - Log.e(TAG,"No buffer available !"); - } - } finally { - mCamera.addCallbackBuffer(data); - } - } - }; - - for (int i=0;i<10;i++) mCamera.addCallbackBuffer(new byte[convertor.getBufferSize()]); - mCamera.setPreviewCallbackWithBuffer(callback); - - // 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; - - } - - /** - * Video encoding is done by a MediaCodec. - * But here we will use the buffer-to-surface method - */ - @SuppressLint({ "InlinedApi", "NewApi" }) - protected void encodeWithMediaCodecMethod2() throws RuntimeException, IOException { - - Log.d(TAG,"Video encoded using the MediaCodec API with a surface"); - - // Updates the parameters of the camera if needed - createCamera(); - updateCamera(); - - // Estimates the framerate of the camera - measureFramerate(); - - EncoderDebugger debugger = EncoderDebugger.debug(mSettings, mQuality.resX, mQuality.resY); - - mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName()); - MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mQuality.resX, mQuality.resY); - mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mQuality.bitrate); - mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mQuality.framerate); - mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); - mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); - mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - Surface surface = mMediaCodec.createInputSurface(); - ((SurfaceView)mSurfaceView).addMediaCodecSurface(surface); - mMediaCodec.start(); - - // 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; - - } - - /** - * Returns a description of the stream using SDP. - * This method can only be called after {@link Stream#configure()}. - * @throws IllegalStateException Thrown when {@link Stream#configure()} wa not called. - */ - public abstract String getSessionDescription() throws IllegalStateException; - - /** - * Opens the camera in a new Looper thread so that the preview callback is not called from the main thread - * If an exception is thrown in this Looper thread, we bring it back into the main thread. - * @throws RuntimeException Might happen if another app is already using the camera. - */ - private void openCamera() throws RuntimeException { - final Semaphore lock = new Semaphore(0); - final RuntimeException[] exception = new RuntimeException[1]; - mCameraThread = new Thread(new Runnable() { - @Override - public void run() { - Looper.prepare(); - mCameraLooper = Looper.myLooper(); - try { - mCamera = Camera.open(mCameraId); - } catch (RuntimeException e) { - exception[0] = e; - } finally { - lock.release(); - Looper.loop(); - } - } - }); - mCameraThread.start(); - lock.acquireUninterruptibly(); - if (exception[0] != null) throw new CameraInUseException(exception[0].getMessage()); - } - - protected synchronized void createCamera() throws RuntimeException { - if (mSurfaceView == null) - throw new InvalidSurfaceException("Invalid surface !"); - if (mSurfaceView.getHolder() == null || !mSurfaceReady) - throw new InvalidSurfaceException("Invalid surface !"); - - if (mCamera == null) { - openCamera(); - mUpdated = false; - mUnlocked = false; - mCamera.setErrorCallback(new Camera.ErrorCallback() { - @Override - public void onError(int error, Camera camera) { - // On some phones when trying to use the camera facing front the media server will die - // Whether or not this callback may be called really depends on the phone - if (error == Camera.CAMERA_ERROR_SERVER_DIED) { - // In this case the application must release the camera and instantiate a new one - Log.e(TAG,"Media server died !"); - // We don't know in what thread we are so stop needs to be synchronized - mCameraOpenedManually = false; - stop(); - } else { - Log.e(TAG,"Error unknown with the camera: "+error); - } - } - }); - - try { - - // If the phone has a flash, we turn it on/off according to mFlashEnabled - // setRecordingHint(true) is a very nice optimization if you plane to only use the Camera for recording - Parameters parameters = mCamera.getParameters(); - if (parameters.getFlashMode()!=null) { - parameters.setFlashMode(mFlashEnabled?Parameters.FLASH_MODE_TORCH:Parameters.FLASH_MODE_OFF); - } - parameters.setRecordingHint(true); - mCamera.setParameters(parameters); - mCamera.setDisplayOrientation(mOrientation); - - try { - if (mMode == MODE_MEDIACODEC_API_2) { - mSurfaceView.startGLThread(); - mCamera.setPreviewTexture(mSurfaceView.getSurfaceTexture()); - } else { - mCamera.setPreviewDisplay(mSurfaceView.getHolder()); - } - } catch (IOException e) { - throw new InvalidSurfaceException("Invalid surface !"); - } - - } catch (RuntimeException e) { - destroyCamera(); - throw e; - } - - } - } - - protected synchronized void destroyCamera() { - if (mCamera != null) { - if (mStreaming) super.stop(); - lockCamera(); - mCamera.stopPreview(); - try { - mCamera.release(); - } catch (Exception e) { - Log.e(TAG,e.getMessage()!=null?e.getMessage():"unknown error"); - } - mCamera = null; - mCameraLooper.quit(); - mUnlocked = false; - mPreviewStarted = false; - } - } - - protected synchronized void updateCamera() throws RuntimeException { - - // The camera is already correctly configured - if (mUpdated) return; - - if (mPreviewStarted) { - mPreviewStarted = false; - mCamera.stopPreview(); - } - - Parameters parameters = mCamera.getParameters(); - mQuality = VideoQuality.determineClosestSupportedResolution(parameters, mQuality); - int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters); - - double ratio = (double)mQuality.resX/(double)mQuality.resY; - mSurfaceView.requestAspectRatio(ratio); - - parameters.setPreviewFormat(mCameraImageFormat); - parameters.setPreviewSize(mQuality.resX, mQuality.resY); - parameters.setPreviewFpsRange(max[0], max[1]); - - try { - mCamera.setParameters(parameters); - mCamera.setDisplayOrientation(mOrientation); - mCamera.startPreview(); - mPreviewStarted = true; - mUpdated = true; - } catch (RuntimeException e) { - destroyCamera(); - throw e; - } - } - - protected void lockCamera() { - if (mUnlocked) { - Log.d(TAG,"Locking camera"); - try { - mCamera.reconnect(); - } catch (Exception e) { - Log.e(TAG,e.getMessage()); - } - mUnlocked = false; - } - } - - protected void unlockCamera() { - if (!mUnlocked) { - Log.d(TAG,"Unlocking camera"); - try { - mCamera.unlock(); - } catch (Exception e) { - Log.e(TAG,e.getMessage()); - } - mUnlocked = true; - } - } - - - /** - * Computes the average frame rate at which the preview callback is called. - * We will then use this average framerate with the MediaCodec. - * Blocks the thread in which this function is called. - */ - private void measureFramerate() { - final Semaphore lock = new Semaphore(0); - - final Camera.PreviewCallback callback = new Camera.PreviewCallback() { - int i = 0, t = 0; - long now, oldnow, count = 0; - @Override - public void onPreviewFrame(byte[] data, Camera camera) { - i++; - now = System.nanoTime()/1000; - if (i>3) { - t += now - oldnow; - count++; - } - if (i>20) { - mQuality.framerate = (int) (1000000/(t/count)+1); - lock.release(); - } - oldnow = now; - } - }; - - mCamera.setPreviewCallback(callback); - - try { - lock.tryAcquire(2,TimeUnit.SECONDS); - Log.d(TAG,"Actual framerate: "+mQuality.framerate); - if (mSettings != null) { - Editor editor = mSettings.edit(); - editor.putInt(PREF_PREFIX+"fps"+mRequestedQuality.framerate+","+mCameraImageFormat+","+mRequestedQuality.resX+mRequestedQuality.resY, mQuality.framerate); - editor.commit(); - } - } catch (InterruptedException e) {} - - mCamera.setPreviewCallback(null); - - } - -} From cec9ed8c86de241149df94289460baf725684d55 Mon Sep 17 00:00:00 2001 From: prakarnwongsanit Date: Mon, 22 Dec 2014 13:12:33 +0700 Subject: [PATCH 2/3] Add all source file to project --- build.gradle | 50 + src/main/AndroidManifest.xml | 10 + .../streaming/MediaStream.java | 339 +++++++ .../majorkernelpanic/streaming/Session.java | 745 +++++++++++++++ .../streaming/SessionBuilder.java | 313 +++++++ .../majorkernelpanic/streaming/Stream.java | 119 +++ .../streaming/audio/AACStream.java | 377 ++++++++ .../streaming/audio/AMRNBStream.java | 89 ++ .../streaming/audio/AudioQuality.java | 70 ++ .../streaming/audio/AudioStream.java | 104 +++ .../exceptions/CameraInUseException.java | 30 + .../exceptions/ConfNotSupportedException.java | 30 + .../exceptions/InvalidSurfaceException.java | 31 + .../StorageUnavailableException.java | 32 + .../streaming/gl/SurfaceManager.java | 196 ++++ .../streaming/gl/SurfaceView.java | 331 +++++++ .../streaming/gl/TextureManager.java | 288 ++++++ .../streaming/hw/CodecManager.java | 160 ++++ .../streaming/hw/EncoderDebugger.java | 859 ++++++++++++++++++ .../streaming/hw/NV21Convertor.java | 166 ++++ .../streaming/mp4/MP4Config.java | 97 ++ .../streaming/mp4/MP4Parser.java | 255 ++++++ .../streaming/rtcp/SenderReport.java | 226 +++++ .../streaming/rtp/AACADTSPacketizer.java | 188 ++++ .../streaming/rtp/AACLATMPacketizer.java | 135 +++ .../streaming/rtp/AMRNBPacketizer.java | 138 +++ .../streaming/rtp/AbstractPacketizer.java | 165 ++++ .../streaming/rtp/H263Packetizer.java | 149 +++ .../streaming/rtp/H264Packetizer.java | 277 ++++++ .../streaming/rtp/MediaCodecInputStream.java | 122 +++ .../streaming/rtp/RtpSocket.java | 451 +++++++++ .../streaming/rtsp/RtcpDeinterleaver.java | 72 ++ .../streaming/rtsp/RtspClient.java | 607 +++++++++++++ .../streaming/rtsp/RtspServer.java | 655 +++++++++++++ .../streaming/rtsp/UriParser.java | 207 +++++ .../streaming/video/CodecManager.java | 265 ++++++ .../streaming/video/H263Stream.java | 86 ++ .../streaming/video/H264Stream.java | 280 ++++++ .../streaming/video/VideoQuality.java | 147 +++ .../streaming/video/VideoStream.java | 729 +++++++++++++++ 40 files changed, 9590 insertions(+) create mode 100644 build.gradle create mode 100644 src/main/AndroidManifest.xml create mode 100644 src/main/java/net/majorkernelpanic/streaming/MediaStream.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/Session.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/SessionBuilder.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/Stream.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/audio/AACStream.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/audio/AMRNBStream.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/audio/AudioQuality.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/audio/AudioStream.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/exceptions/CameraInUseException.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/exceptions/ConfNotSupportedException.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/exceptions/InvalidSurfaceException.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/exceptions/StorageUnavailableException.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/gl/SurfaceManager.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/gl/SurfaceView.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/gl/TextureManager.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/hw/CodecManager.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/hw/EncoderDebugger.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/hw/NV21Convertor.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/mp4/MP4Config.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/mp4/MP4Parser.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtcp/SenderReport.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtp/AACADTSPacketizer.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtp/AACLATMPacketizer.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtp/AMRNBPacketizer.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtp/AbstractPacketizer.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtp/H263Packetizer.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtp/H264Packetizer.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtp/MediaCodecInputStream.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtp/RtpSocket.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtsp/RtcpDeinterleaver.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtsp/RtspClient.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtsp/RtspServer.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/rtsp/UriParser.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/video/CodecManager.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/video/H263Stream.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/video/H264Stream.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/video/VideoQuality.java create mode 100644 src/main/java/net/majorkernelpanic/streaming/video/VideoStream.java diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..d0a36567 --- /dev/null +++ b/build.gradle @@ -0,0 +1,50 @@ +apply plugin: 'com.android.library' +apply plugin: 'maven' + + +group = 'net.majorkernelpanic' +version = '3.0' + +description = """libstreaming""" + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0' + } +} + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 21 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } +} + +repositories { + mavenCentral() +} + + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) +} + +uploadArchives { + repositories { + mavenDeployer { + repository url: 'file://' + new File(System.getProperty('user.home'), '.m3/repository').absolutePath + } + } +} +task install(dependsOn: uploadArchives) \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9e2e14dc --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/src/main/java/net/majorkernelpanic/streaming/MediaStream.java b/src/main/java/net/majorkernelpanic/streaming/MediaStream.java new file mode 100644 index 00000000..95fcecdb --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/MediaStream.java @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.util.Random; + +import net.majorkernelpanic.streaming.audio.AudioStream; +import net.majorkernelpanic.streaming.rtp.AbstractPacketizer; +import net.majorkernelpanic.streaming.video.VideoStream; +import android.annotation.SuppressLint; +import android.media.MediaCodec; +import android.media.MediaRecorder; +import android.net.LocalServerSocket; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.util.Log; + +/** + * A MediaRecorder that streams what it records using a packetizer from the RTP package. + * You can't use this class directly ! + */ +public abstract class MediaStream implements Stream { + + protected static final String TAG = "MediaStream"; + + /** Raw audio/video will be encoded using the MediaRecorder API. */ + public static final byte MODE_MEDIARECORDER_API = 0x01; + + /** Raw audio/video will be encoded using the MediaCodec API with buffers. */ + public static final byte MODE_MEDIACODEC_API = 0x02; + + /** Raw audio/video will be encoded using the MediaCode API with a surface. */ + public static final byte MODE_MEDIACODEC_API_2 = 0x05; + + /** Prefix that will be used for all shared preferences saved by libstreaming */ + protected static final String PREF_PREFIX = "libstreaming-"; + + /** The packetizer that will read the output of the camera and send RTP packets over the networked. */ + protected AbstractPacketizer mPacketizer = null; + + protected static byte sSuggestedMode = MODE_MEDIARECORDER_API; + protected byte mMode, mRequestedMode; + + protected boolean mStreaming = false, mConfigured = false; + protected int mRtpPort = 0, mRtcpPort = 0; + protected byte mChannelIdentifier = 0; + protected OutputStream mOutputStream = null; + protected InetAddress mDestination; + protected LocalSocket mReceiver, mSender = null; + private LocalServerSocket mLss = null; + private int mSocketId, mTTL = 64; + + protected MediaRecorder mMediaRecorder; + protected MediaCodec mMediaCodec; + + static { + // We determine whether or not the MediaCodec API should be used + try { + Class.forName("android.media.MediaCodec"); + // Will be set to MODE_MEDIACODEC_API at some point... + sSuggestedMode = MODE_MEDIACODEC_API; + Log.i(TAG,"Phone supports the MediaCoded API"); + } catch (ClassNotFoundException e) { + sSuggestedMode = MODE_MEDIARECORDER_API; + Log.i(TAG,"Phone does not support the MediaCodec API"); + } + } + + public MediaStream() { + mRequestedMode = sSuggestedMode; + mMode = sSuggestedMode; + } + + /** + * Sets the destination ip address of the stream. + * @param dest The destination address of the stream + */ + public void setDestinationAddress(InetAddress dest) { + mDestination = dest; + } + + /** + * Sets the destination ports of the stream. + * If an odd number is supplied for the destination port then the next + * lower even number will be used for RTP and it will be used for RTCP. + * If an even number is supplied, it will be used for RTP and the next odd + * number will be used for RTCP. + * @param dport The destination port + */ + public void setDestinationPorts(int dport) { + if (dport % 2 == 1) { + mRtpPort = dport-1; + mRtcpPort = dport; + } else { + mRtpPort = dport; + mRtcpPort = dport+1; + } + } + + /** + * Sets the destination ports of the stream. + * @param rtpPort Destination port that will be used for RTP + * @param rtcpPort Destination port that will be used for RTCP + */ + public void setDestinationPorts(int rtpPort, int rtcpPort) { + mRtpPort = rtpPort; + mRtcpPort = rtcpPort; + mOutputStream = null; + } + + /** + * If a TCP is used as the transport protocol for the RTP session, + * the output stream to which RTP packets will be written to must + * be specified with this method. + */ + public void setOutputStream(OutputStream stream, byte channelIdentifier) { + mOutputStream = stream; + mChannelIdentifier = channelIdentifier; + } + + + /** + * Sets the Time To Live of packets sent over the network. + * @param ttl The time to live + * @throws IOException + */ + public void setTimeToLive(int ttl) throws IOException { + mTTL = ttl; + } + + /** + * Returns a pair of destination ports, the first one is the + * one used for RTP and the second one is used for RTCP. + **/ + public int[] getDestinationPorts() { + return new int[] { + mRtpPort, + mRtcpPort + }; + } + + /** + * Returns a pair of source ports, the first one is the + * one used for RTP and the second one is used for RTCP. + **/ + public int[] getLocalPorts() { + return mPacketizer.getRtpSocket().getLocalPorts(); + } + + /** + * Sets the streaming method that will be used. + * + * If the mode is set to {@link #MODE_MEDIARECORDER_API}, raw audio/video will be encoded + * using the MediaRecorder API.
+ * + * If the mode is set to {@link #MODE_MEDIACODEC_API} or to {@link #MODE_MEDIACODEC_API_2}, + * audio/video will be encoded with using the MediaCodec.
+ * + * The {@link #MODE_MEDIACODEC_API_2} mode only concerns {@link VideoStream}, it makes + * use of the createInputSurface() method of the MediaCodec API (Android 4.3 is needed there).
+ * + * @param mode Can be {@link #MODE_MEDIARECORDER_API}, {@link #MODE_MEDIACODEC_API} or {@link #MODE_MEDIACODEC_API_2} + */ + public void setStreamingMethod(byte mode) { + mRequestedMode = mode; + } + + /** + * Returns the streaming method in use, call this after + * {@link #configure()} to get an accurate response. + */ + public byte getStreamingMethod() { + return mMode; + } + + /** + * Returns the packetizer associated with the {@link MediaStream}. + * @return The packetizer + */ + public AbstractPacketizer getPacketizer() { + return mPacketizer; + } + + /** + * Returns an approximation of the bit rate consumed by the stream in bit per seconde. + */ + public long getBitrate() { + return !mStreaming ? 0 : mPacketizer.getRtpSocket().getBitrate(); + } + + /** + * Indicates if the {@link MediaStream} is streaming. + * @return A boolean indicating if the {@link MediaStream} is streaming + */ + public boolean isStreaming() { + return mStreaming; + } + + /** + * Configures the stream with the settings supplied with + * {@link VideoStream#setVideoQuality(net.majorkernelpanic.streaming.video.VideoQuality)} + * for a {@link VideoStream} and {@link AudioStream#setAudioQuality(net.majorkernelpanic.streaming.audio.AudioQuality)} + * for a {@link AudioStream}. + */ + public synchronized void configure() throws IllegalStateException, IOException { + if (mStreaming) throw new IllegalStateException("Can't be called while streaming."); + if (mPacketizer != null) { + mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort); + mPacketizer.getRtpSocket().setOutputStream(mOutputStream, mChannelIdentifier); + } + mMode = mRequestedMode; + mConfigured = true; + } + + /** Starts the stream. */ + public synchronized void start() throws IllegalStateException, IOException { + + if (mDestination==null) + throw new IllegalStateException("No destination ip address set for the stream !"); + + if (mRtpPort<=0 || mRtcpPort<=0) + throw new IllegalStateException("No destination ports set for the stream !"); + + mPacketizer.setTimeToLive(mTTL); + + if (mMode != MODE_MEDIARECORDER_API) { + encodeWithMediaCodec(); + } else { + encodeWithMediaRecorder(); + } + + } + + /** Stops the stream. */ + @SuppressLint("NewApi") + public synchronized void stop() { + if (mStreaming) { + try { + if (mMode==MODE_MEDIARECORDER_API) { + mMediaRecorder.stop(); + mMediaRecorder.release(); + mMediaRecorder = null; + closeSockets(); + mPacketizer.stop(); + } else { + mPacketizer.stop(); + mMediaCodec.stop(); + mMediaCodec.release(); + mMediaCodec = null; + } + } catch (Exception e) { + e.printStackTrace(); + } + mStreaming = false; + } + } + + protected abstract void encodeWithMediaRecorder() throws IOException; + + protected abstract void encodeWithMediaCodec() throws IOException; + + /** + * Returns a description of the stream using SDP. + * This method can only be called after {@link Stream#configure()}. + * @throws IllegalStateException Thrown when {@link Stream#configure()} was not called. + */ + public abstract String getSessionDescription(); + + /** + * Returns the SSRC of the underlying {@link net.majorkernelpanic.streaming.rtp.RtpSocket}. + * @return the SSRC of the stream + */ + public int getSSRC() { + return getPacketizer().getSSRC(); + } + + protected void createSockets() throws IOException { + + final String LOCAL_ADDR = "net.majorkernelpanic.streaming-"; + + for (int i=0;i<10;i++) { + try { + mSocketId = new Random().nextInt(); + mLss = new LocalServerSocket(LOCAL_ADDR+mSocketId); + break; + } catch (IOException e1) {} + } + + mReceiver = new LocalSocket(); + mReceiver.connect( new LocalSocketAddress(LOCAL_ADDR+mSocketId)); + mReceiver.setReceiveBufferSize(500000); + mReceiver.setSoTimeout(3000); + mSender = mLss.accept(); + mSender.setSendBufferSize(500000); + } + + protected void closeSockets() { + try { + mReceiver.close(); + } catch (Exception e) { + e.printStackTrace(); + } + try { + mSender.close(); + } catch (Exception e) { + e.printStackTrace(); + } + try { + mLss.close(); + } catch (Exception e) { + e.printStackTrace(); + } + mLss = null; + mSender = null; + mReceiver = null; + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/Session.java b/src/main/java/net/majorkernelpanic/streaming/Session.java new file mode 100644 index 00000000..a904fd4f --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/Session.java @@ -0,0 +1,745 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.concurrent.CountDownLatch; + +import net.majorkernelpanic.streaming.audio.AudioQuality; +import net.majorkernelpanic.streaming.audio.AudioStream; +import net.majorkernelpanic.streaming.exceptions.CameraInUseException; +import net.majorkernelpanic.streaming.exceptions.ConfNotSupportedException; +import net.majorkernelpanic.streaming.exceptions.InvalidSurfaceException; +import net.majorkernelpanic.streaming.exceptions.StorageUnavailableException; +import net.majorkernelpanic.streaming.gl.SurfaceView; +import net.majorkernelpanic.streaming.rtsp.RtspClient; +import net.majorkernelpanic.streaming.video.VideoQuality; +import net.majorkernelpanic.streaming.video.VideoStream; +import android.hardware.Camera.CameraInfo; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; + +/** + * You should instantiate this class with the {@link SessionBuilder}.
+ * This is the class you will want to use to stream audio and or video to some peer using RTP.
+ * + * It holds a {@link VideoStream} and a {@link AudioStream} together and provides + * synchronous and asynchronous functions to start and stop those steams. + * You should implement a callback interface {@link Callback} to receive notifications and error reports.
+ * + * If you want to stream to a RTSP server, you will need an instance of this class and hand it to a {@link RtspClient}. + * + * If you don't use the RTSP protocol, you will still need to send a session description to the receiver + * for him to be able to decode your audio/video streams. You can obtain this session description by calling + * {@link #configure()} or {@link #syncConfigure()} to configure the session with its parameters + * (audio samplingrate, video resolution) and then {@link Session#getSessionDescription()}.
+ * + * See the example 2 here: https://github.com/fyhertz/libstreaming-examples to + * see an example of how to get a SDP.
+ * + * See the example 3 here: https://github.com/fyhertz/libstreaming-examples to + * see an example of how to stream to a RTSP server.
+ * + */ +public class Session { + + public final static String TAG = "Session"; + + public final static int STREAM_VIDEO = 0x01; + + public final static int STREAM_AUDIO = 0x00; + + /** Some app is already using a camera (Camera.open() has failed). */ + public final static int ERROR_CAMERA_ALREADY_IN_USE = 0x00; + + /** The phone may not support some streaming parameters that you are trying to use (bit rate, frame rate, resolution...). */ + public final static int ERROR_CONFIGURATION_NOT_SUPPORTED = 0x01; + + /** + * The internal storage of the phone is not ready. + * libstreaming tried to store a test file on the sdcard but couldn't. + * See H264Stream and AACStream to find out why libstreaming would want to something like that. + */ + public final static int ERROR_STORAGE_NOT_READY = 0x02; + + /** The phone has no flash. */ + public final static int ERROR_CAMERA_HAS_NO_FLASH = 0x03; + + /** The supplied SurfaceView is not a valid surface, or has not been created yet. */ + public final static int ERROR_INVALID_SURFACE = 0x04; + + /** + * The destination set with {@link Session#setDestination(String)} could not be resolved. + * May mean that the phone has no access to the internet, or that the DNS server could not + * resolved the host name. + */ + public final static int ERROR_UNKNOWN_HOST = 0x05; + + /** + * Some other error occurred ! + */ + public final static int ERROR_OTHER = 0x06; + + private String mOrigin; + private String mDestination; + private int mTimeToLive = 64; + private long mTimestamp; + + private AudioStream mAudioStream = null; + private VideoStream mVideoStream = null; + + private Callback mCallback; + private Handler mMainHandler; + + private Handler mHandler; + + /** + * Creates a streaming session that can be customized by adding tracks. + */ + public Session() { + long uptime = System.currentTimeMillis(); + + HandlerThread thread = new HandlerThread("net.majorkernelpanic.streaming.Session"); + thread.start(); + + mHandler = new Handler(thread.getLooper()); + mMainHandler = new Handler(Looper.getMainLooper()); + mTimestamp = (uptime/1000)<<32 & (((uptime-((uptime/1000)*1000))>>32)/1000); // NTP timestamp + mOrigin = "127.0.0.1"; + } + + /** + * The callback interface you need to implement to get some feedback + * Those will be called from the UI thread. + */ + public interface Callback { + + /** + * Called periodically to inform you on the bandwidth + * consumption of the streams when streaming. + */ + public void onBitrateUpdate(long bitrate); + + /** Called when some error occurs. */ + public void onSessionError(int reason, int streamType, Exception e); + + /** + * Called when the previw of the {@link VideoStream} + * has correctly been started. + * If an error occurs while starting the preview, + * {@link Callback#onSessionError(int, int, Exception)} will be + * called instead of {@link Callback#onPreviewStarted()}. + */ + public void onPreviewStarted(); + + /** + * Called when the session has correctly been configured + * after calling {@link Session#configure()}. + * If an error occurs while configuring the {@link Session}, + * {@link Callback#onSessionError(int, int, Exception)} will be + * called instead of {@link Callback#onSessionConfigured()}. + */ + public void onSessionConfigured(); + + /** + * Called when the streams of the session have correctly been started. + * If an error occurs while starting the {@link Session}, + * {@link Callback#onSessionError(int, int, Exception)} will be + * called instead of {@link Callback#onSessionStarted()}. + */ + public void onSessionStarted(); + + /** Called when the stream of the session have been stopped. */ + public void onSessionStopped(); + + } + + /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ + void addAudioTrack(AudioStream track) { + removeAudioTrack(); + mAudioStream = track; + } + + /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ + void addVideoTrack(VideoStream track) { + removeVideoTrack(); + mVideoStream = track; + } + + /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ + void removeAudioTrack() { + if (mAudioStream != null) { + mAudioStream.stop(); + mAudioStream = null; + } + } + + /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ + void removeVideoTrack() { + if (mVideoStream != null) { + mVideoStream.stopPreview(); + mVideoStream = null; + } + } + + /** Returns the underlying {@link AudioStream} used by the {@link Session}. */ + public AudioStream getAudioTrack() { + return mAudioStream; + } + + /** Returns the underlying {@link VideoStream} used by the {@link Session}. */ + public VideoStream getVideoTrack() { + return mVideoStream; + } + + /** + * Sets the callback interface that will be called by the {@link Session}. + * @param callback The implementation of the {@link Callback} interface + */ + public void setCallback(Callback callback) { + mCallback = callback; + } + + /** + * The origin address of the session. + * It appears in the session description. + * @param origin The origin address + */ + public void setOrigin(String origin) { + mOrigin = origin; + } + + /** + * The destination address for all the streams of the session.
+ * Changes will be taken into account the next time you start the session. + * @param destination The destination address + */ + public void setDestination(String destination) { + mDestination = destination; + } + + /** + * Set the TTL of all packets sent during the session.
+ * Changes will be taken into account the next time you start the session. + * @param ttl The Time To Live + */ + public void setTimeToLive(int ttl) { + mTimeToLive = ttl; + } + + /** + * Sets the configuration of the stream.
+ * You can call this method at any time and changes will take + * effect next time you call {@link #configure()}. + * @param quality Quality of the stream + */ + public void setVideoQuality(VideoQuality quality) { + if (mVideoStream != null) { + mVideoStream.setVideoQuality(quality); + } + } + + /** + * Sets a Surface to show a preview of recorded media (video).
+ * You can call this method at any time and changes will take + * effect next time you call {@link #start()} or {@link #startPreview()}. + */ + public void setSurfaceView(final SurfaceView view) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mVideoStream != null) { + mVideoStream.setSurfaceView(view); + } + } + }); + } + + /** + * Sets the orientation of the preview.
+ * You can call this method at any time and changes will take + * effect next time you call {@link #configure()}. + * @param orientation The orientation of the preview + */ + public void setPreviewOrientation(int orientation) { + if (mVideoStream != null) { + mVideoStream.setPreviewOrientation(orientation); + } + } + + /** + * Sets the configuration of the stream.
+ * You can call this method at any time and changes will take + * effect next time you call {@link #configure()}. + * @param quality Quality of the stream + */ + public void setAudioQuality(AudioQuality quality) { + if (mAudioStream != null) { + mAudioStream.setAudioQuality(quality); + } + } + + /** + * Returns the {@link Callback} interface that was set with + * {@link #setCallback(Callback)} or null if none was set. + */ + public Callback getCallback() { + return mCallback; + } + + /** + * Returns a Session Description that can be stored in a file or sent to a client with RTSP. + * @return The Session Description. + * @throws IllegalStateException Thrown when {@link #setDestination(String)} has never been called. + */ + public String getSessionDescription() { + StringBuilder sessionDescription = new StringBuilder(); + if (mDestination==null) { + throw new IllegalStateException("setDestination() has not been called !"); + } + sessionDescription.append("v=0\r\n"); + // TODO: Add IPV6 support + sessionDescription.append("o=- "+mTimestamp+" "+mTimestamp+" IN IP4 "+mOrigin+"\r\n"); + sessionDescription.append("s=Unnamed\r\n"); + sessionDescription.append("i=N/A\r\n"); + sessionDescription.append("c=IN IP4 "+mDestination+"\r\n"); + // t=0 0 means the session is permanent (we don't know when it will stop) + sessionDescription.append("t=0 0\r\n"); + sessionDescription.append("a=recvonly\r\n"); + // Prevents two different sessions from using the same peripheral at the same time + if (mAudioStream != null) { + sessionDescription.append(mAudioStream.getSessionDescription()); + sessionDescription.append("a=control:trackID="+0+"\r\n"); + } + if (mVideoStream != null) { + sessionDescription.append(mVideoStream.getSessionDescription()); + sessionDescription.append("a=control:trackID="+1+"\r\n"); + } + return sessionDescription.toString(); + } + + /** Returns the destination set with {@link #setDestination(String)}. */ + public String getDestination() { + return mDestination; + } + + /** Returns an approximation of the bandwidth consumed by the session in bit per second. */ + public long getBitrate() { + long sum = 0; + if (mAudioStream != null) sum += mAudioStream.getBitrate(); + if (mVideoStream != null) sum += mVideoStream.getBitrate(); + return sum; + } + + /** Indicates if a track is currently running. */ + public boolean isStreaming() { + if ( (mAudioStream!=null && mAudioStream.isStreaming()) || (mVideoStream!=null && mVideoStream.isStreaming()) ) + return true; + else + return false; + } + + /** + * Configures all streams of the session. + **/ + public void configure() { + mHandler.post(new Runnable() { + @Override + public void run() { + try { + syncConfigure(); + } catch (Exception e) {}; + } + }); + } + + /** + * Does the same thing as {@link #configure()}, but in a synchronous manner.
+ * Throws exceptions in addition to calling a callback + * {@link Callback#onSessionError(int, int, Exception)} when + * an error occurs. + **/ + public void syncConfigure() + throws CameraInUseException, + StorageUnavailableException, + ConfNotSupportedException, + InvalidSurfaceException, + RuntimeException, + IOException { + + for (int id=0;id<2;id++) { + Stream stream = id==0 ? mAudioStream : mVideoStream; + if (stream!=null && !stream.isStreaming()) { + try { + stream.configure(); + } catch (CameraInUseException e) { + postError(ERROR_CAMERA_ALREADY_IN_USE , id, e); + throw e; + } catch (StorageUnavailableException e) { + postError(ERROR_STORAGE_NOT_READY , id, e); + throw e; + } catch (ConfNotSupportedException e) { + postError(ERROR_CONFIGURATION_NOT_SUPPORTED , id, e); + throw e; + } catch (InvalidSurfaceException e) { + postError(ERROR_INVALID_SURFACE , id, e); + throw e; + } catch (IOException e) { + postError(ERROR_OTHER, id, e); + throw e; + } catch (RuntimeException e) { + postError(ERROR_OTHER, id, e); + throw e; + } + } + } + postSessionConfigured(); + } + + /** + * Asynchronously starts all streams of the session. + **/ + public void start() { + mHandler.post(new Runnable() { + @Override + public void run() { + try { + syncStart(); + } catch (Exception e) {} + } + }); + } + + /** + * Starts a stream in a synchronous manner.
+ * Throws exceptions in addition to calling a callback. + * @param id The id of the stream to start + **/ + public void syncStart(int id) + throws CameraInUseException, + StorageUnavailableException, + ConfNotSupportedException, + InvalidSurfaceException, + UnknownHostException, + IOException { + + Stream stream = id==0 ? mAudioStream : mVideoStream; + if (stream!=null && !stream.isStreaming()) { + try { + InetAddress destination = InetAddress.getByName(mDestination); + stream.setTimeToLive(mTimeToLive); + stream.setDestinationAddress(destination); + stream.start(); + if (getTrack(1-id) == null || getTrack(1-id).isStreaming()) { + postSessionStarted(); + } + if (getTrack(1-id) == null || !getTrack(1-id).isStreaming()) { + mHandler.post(mUpdateBitrate); + } + } catch (UnknownHostException e) { + postError(ERROR_UNKNOWN_HOST, id, e); + throw e; + } catch (CameraInUseException e) { + postError(ERROR_CAMERA_ALREADY_IN_USE , id, e); + throw e; + } catch (StorageUnavailableException e) { + postError(ERROR_STORAGE_NOT_READY , id, e); + throw e; + } catch (ConfNotSupportedException e) { + postError(ERROR_CONFIGURATION_NOT_SUPPORTED , id, e); + throw e; + } catch (InvalidSurfaceException e) { + postError(ERROR_INVALID_SURFACE , id, e); + throw e; + } catch (IOException e) { + postError(ERROR_OTHER, id, e); + throw e; + } catch (RuntimeException e) { + postError(ERROR_OTHER, id, e); + throw e; + } + } + + } + + /** + * Does the same thing as {@link #start()}, but in a synchronous manner.
+ * Throws exceptions in addition to calling a callback. + **/ + public void syncStart() + throws CameraInUseException, + StorageUnavailableException, + ConfNotSupportedException, + InvalidSurfaceException, + UnknownHostException, + IOException { + + syncStart(1); + try { + syncStart(0); + } catch (RuntimeException e) { + syncStop(1); + throw e; + } catch (IOException e) { + syncStop(1); + throw e; + } + + } + + /** Stops all existing streams. */ + public void stop() { + mHandler.post(new Runnable() { + @Override + public void run() { + syncStop(); + } + }); + } + + /** + * Stops one stream in a synchronous manner. + * @param id The id of the stream to stop + **/ + private void syncStop(final int id) { + Stream stream = id==0 ? mAudioStream : mVideoStream; + if (stream!=null) { + stream.stop(); + } + } + + /** Stops all existing streams in a synchronous manner. */ + public void syncStop() { + syncStop(0); + syncStop(1); + postSessionStopped(); + } + + /** + * Asynchronously starts the camera preview.
+ * You should of course pass a {@link SurfaceView} to {@link #setSurfaceView(SurfaceView)} + * before calling this method. Otherwise, the {@link Callback#onSessionError(int, int, Exception)} + * callback will be called with {@link #ERROR_INVALID_SURFACE}. + */ + public void startPreview() { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mVideoStream != null) { + try { + mVideoStream.startPreview(); + postPreviewStarted(); + mVideoStream.configure(); + } catch (CameraInUseException e) { + postError(ERROR_CAMERA_ALREADY_IN_USE , STREAM_VIDEO, e); + } catch (ConfNotSupportedException e) { + postError(ERROR_CONFIGURATION_NOT_SUPPORTED , STREAM_VIDEO, e); + } catch (InvalidSurfaceException e) { + postError(ERROR_INVALID_SURFACE , STREAM_VIDEO, e); + } catch (RuntimeException e) { + postError(ERROR_OTHER, STREAM_VIDEO, e); + } catch (StorageUnavailableException e) { + postError(ERROR_STORAGE_NOT_READY, STREAM_VIDEO, e); + } catch (IOException e) { + postError(ERROR_OTHER, STREAM_VIDEO, e); + } + } + } + }); + } + + /** + * Asynchronously stops the camera preview. + */ + public void stopPreview() { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mVideoStream != null) { + mVideoStream.stopPreview(); + } + } + }); + } + + /** Switch between the front facing and the back facing camera of the phone.
+ * If {@link #startPreview()} has been called, the preview will be briefly interrupted.
+ * If {@link #start()} has been called, the stream will be briefly interrupted.
+ * To find out which camera is currently selected, use {@link #getCamera()} + **/ + public void switchCamera() { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mVideoStream != null) { + try { + mVideoStream.switchCamera(); + postPreviewStarted(); + } catch (CameraInUseException e) { + postError(ERROR_CAMERA_ALREADY_IN_USE , STREAM_VIDEO, e); + } catch (ConfNotSupportedException e) { + postError(ERROR_CONFIGURATION_NOT_SUPPORTED , STREAM_VIDEO, e); + } catch (InvalidSurfaceException e) { + postError(ERROR_INVALID_SURFACE , STREAM_VIDEO, e); + } catch (IOException e) { + postError(ERROR_OTHER, STREAM_VIDEO, e); + } catch (RuntimeException e) { + postError(ERROR_OTHER, STREAM_VIDEO, e); + } + } + } + }); + } + + /** + * Returns the id of the camera currently selected.
+ * It can be either {@link CameraInfo#CAMERA_FACING_BACK} or + * {@link CameraInfo#CAMERA_FACING_FRONT}. + */ + public int getCamera() { + return mVideoStream != null ? mVideoStream.getCamera() : 0; + + } + + /** + * Toggles the LED of the phone if it has one. + * You can get the current state of the flash with + * {@link Session#getVideoTrack()} and {@link VideoStream#getFlashState()}. + **/ + public void toggleFlash() { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mVideoStream != null) { + try { + mVideoStream.toggleFlash(); + } catch (RuntimeException e) { + postError(ERROR_CAMERA_HAS_NO_FLASH, STREAM_VIDEO, e); + } + } + } + }); + } + + /** Deletes all existing tracks & release associated resources. */ + public void release() { + removeAudioTrack(); + removeVideoTrack(); + mHandler.getLooper().quit(); + } + + private void postPreviewStarted() { + mMainHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onPreviewStarted(); + } + } + }); + } + + private void postSessionConfigured() { + mMainHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onSessionConfigured(); + } + } + }); + } + + private void postSessionStarted() { + mMainHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onSessionStarted(); + } + } + }); + } + + private void postSessionStopped() { + mMainHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onSessionStopped(); + } + } + }); + } + + private void postError(final int reason, final int streamType,final Exception e) { + mMainHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onSessionError(reason, streamType, e); + } + } + }); + } + + private void postBitRate(final long bitrate) { + mMainHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onBitrateUpdate(bitrate); + } + } + }); + } + + private Runnable mUpdateBitrate = new Runnable() { + @Override + public void run() { + if (isStreaming()) { + postBitRate(getBitrate()); + mHandler.postDelayed(mUpdateBitrate, 500); + } else { + postBitRate(0); + } + } + }; + + + public boolean trackExists(int id) { + if (id==0) + return mAudioStream!=null; + else + return mVideoStream!=null; + } + + public Stream getTrack(int id) { + if (id==0) + return mAudioStream; + else + return mVideoStream; + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/SessionBuilder.java b/src/main/java/net/majorkernelpanic/streaming/SessionBuilder.java new file mode 100644 index 00000000..1ba39c41 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/SessionBuilder.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming; + +import java.io.IOException; +import java.net.InetAddress; + +import net.majorkernelpanic.streaming.audio.AACStream; +import net.majorkernelpanic.streaming.audio.AMRNBStream; +import net.majorkernelpanic.streaming.audio.AudioQuality; +import net.majorkernelpanic.streaming.audio.AudioStream; +import net.majorkernelpanic.streaming.gl.SurfaceView; +import net.majorkernelpanic.streaming.video.H263Stream; +import net.majorkernelpanic.streaming.video.H264Stream; +import net.majorkernelpanic.streaming.video.VideoQuality; +import net.majorkernelpanic.streaming.video.VideoStream; +import android.content.Context; +import android.hardware.Camera.CameraInfo; +import android.preference.PreferenceManager; + +/** + * Call {@link #getInstance()} to get access to the SessionBuilder. + */ +public class SessionBuilder { + + public final static String TAG = "SessionBuilder"; + + /** Can be used with {@link #setVideoEncoder}. */ + public final static int VIDEO_NONE = 0; + + /** Can be used with {@link #setVideoEncoder}. */ + public final static int VIDEO_H264 = 1; + + /** Can be used with {@link #setVideoEncoder}. */ + public final static int VIDEO_H263 = 2; + + /** Can be used with {@link #setAudioEncoder}. */ + public final static int AUDIO_NONE = 0; + + /** Can be used with {@link #setAudioEncoder}. */ + public final static int AUDIO_AMRNB = 3; + + /** Can be used with {@link #setAudioEncoder}. */ + public final static int AUDIO_AAC = 5; + + // Default configuration + private VideoQuality mVideoQuality = VideoQuality.DEFAULT_VIDEO_QUALITY; + private AudioQuality mAudioQuality = AudioQuality.DEFAULT_AUDIO_QUALITY; + private Context mContext; + private int mVideoEncoder = VIDEO_H263; + private int mAudioEncoder = AUDIO_AMRNB; + private int mCamera = CameraInfo.CAMERA_FACING_BACK; + private int mTimeToLive = 64; + private int mOrientation = 0; + private boolean mFlash = false; + private SurfaceView mSurfaceView = null; + private String mOrigin = null; + private String mDestination = null; + private Session.Callback mCallback = null; + + // Removes the default public constructor + private SessionBuilder() {} + + // The SessionManager implements the singleton pattern + private static volatile SessionBuilder sInstance = null; + + /** + * Returns a reference to the {@link SessionBuilder}. + * @return The reference to the {@link SessionBuilder} + */ + public final static SessionBuilder getInstance() { + if (sInstance == null) { + synchronized (SessionBuilder.class) { + if (sInstance == null) { + SessionBuilder.sInstance = new SessionBuilder(); + } + } + } + return sInstance; + } + + /** + * Creates a new {@link Session}. + * @return The new Session + * @throws IOException + */ + public Session build() { + Session session; + + session = new Session(); + session.setOrigin(mOrigin); + session.setDestination(mDestination); + session.setTimeToLive(mTimeToLive); + session.setCallback(mCallback); + + switch (mAudioEncoder) { + case AUDIO_AAC: + AACStream stream = new AACStream(); + session.addAudioTrack(stream); + if (mContext!=null) + stream.setPreferences(PreferenceManager.getDefaultSharedPreferences(mContext)); + break; + case AUDIO_AMRNB: + session.addAudioTrack(new AMRNBStream()); + break; + } + + switch (mVideoEncoder) { + case VIDEO_H263: + session.addVideoTrack(new H263Stream(mCamera)); + break; + case VIDEO_H264: + H264Stream stream = new H264Stream(mCamera); + if (mContext!=null) + stream.setPreferences(PreferenceManager.getDefaultSharedPreferences(mContext)); + session.addVideoTrack(stream); + break; + } + + if (session.getVideoTrack()!=null) { + VideoStream video = session.getVideoTrack(); + video.setFlashState(mFlash); + video.setVideoQuality(mVideoQuality); + video.setSurfaceView(mSurfaceView); + video.setPreviewOrientation(mOrientation); + video.setDestinationPorts(5006); + } + + if (session.getAudioTrack()!=null) { + AudioStream audio = session.getAudioTrack(); + audio.setAudioQuality(mAudioQuality); + audio.setDestinationPorts(5004); + } + + return session; + + } + + /** + * Access to the context is needed for the H264Stream class to store some stuff in the SharedPreferences. + * Note that you should pass the Application context, not the context of an Activity. + **/ + public SessionBuilder setContext(Context context) { + mContext = context; + return this; + } + + /** Sets the destination of the session. */ + public SessionBuilder setDestination(String destination) { + mDestination = destination; + return this; + } + + /** Sets the origin of the session. It appears in the SDP of the session. */ + public SessionBuilder setOrigin(String origin) { + mOrigin = origin; + return this; + } + + /** Sets the video stream quality. */ + public SessionBuilder setVideoQuality(VideoQuality quality) { + mVideoQuality = quality.clone(); + return this; + } + + /** Sets the audio encoder. */ + public SessionBuilder setAudioEncoder(int encoder) { + mAudioEncoder = encoder; + return this; + } + + /** Sets the audio quality. */ + public SessionBuilder setAudioQuality(AudioQuality quality) { + mAudioQuality = quality.clone(); + return this; + } + + /** Sets the default video encoder. */ + public SessionBuilder setVideoEncoder(int encoder) { + mVideoEncoder = encoder; + return this; + } + + public SessionBuilder setFlashEnabled(boolean enabled) { + mFlash = enabled; + return this; + } + + public SessionBuilder setCamera(int camera) { + mCamera = camera; + return this; + } + + public SessionBuilder setTimeToLive(int ttl) { + mTimeToLive = ttl; + return this; + } + + /** + * Sets the SurfaceView required to preview the video stream. + **/ + public SessionBuilder setSurfaceView(SurfaceView surfaceView) { + mSurfaceView = surfaceView; + return this; + } + + /** + * Sets the orientation of the preview. + * @param orientation The orientation of the preview + */ + public SessionBuilder setPreviewOrientation(int orientation) { + mOrientation = orientation; + return this; + } + + public SessionBuilder setCallback(Session.Callback callback) { + mCallback = callback; + return this; + } + + /** Returns the context set with {@link #setContext(Context)}*/ + public Context getContext() { + return mContext; + } + + /** Returns the destination ip address set with {@link #setDestination(String)}. */ + public String getDestination() { + return mDestination; + } + + /** Returns the origin ip address set with {@link #setOrigin(String)}. */ + public String getOrigin() { + return mOrigin; + } + + /** Returns the audio encoder set with {@link #setAudioEncoder(int)}. */ + public int getAudioEncoder() { + return mAudioEncoder; + } + + /** Returns the id of the {@link android.hardware.Camera} set with {@link #setCamera(int)}. */ + public int getCamera() { + return mCamera; + } + + /** Returns the video encoder set with {@link #setVideoEncoder(int)}. */ + public int getVideoEncoder() { + return mVideoEncoder; + } + + /** Returns the VideoQuality set with {@link #setVideoQuality(VideoQuality)}. */ + public VideoQuality getVideoQuality() { + return mVideoQuality; + } + + /** Returns the AudioQuality set with {@link #setAudioQuality(AudioQuality)}. */ + public AudioQuality getAudioQuality() { + return mAudioQuality; + } + + /** Returns the flash state set with {@link #setFlashEnabled(boolean)}. */ + public boolean getFlashState() { + return mFlash; + } + + /** Returns the SurfaceView set with {@link #setSurfaceView(SurfaceView)}. */ + public SurfaceView getSurfaceView() { + return mSurfaceView; + } + + + /** Returns the time to live set with {@link #setTimeToLive(int)}. */ + public int getTimeToLive() { + return mTimeToLive; + } + + /** Returns a new {@link SessionBuilder} with the same configuration. */ + public SessionBuilder clone() { + return new SessionBuilder() + .setDestination(mDestination) + .setOrigin(mOrigin) + .setSurfaceView(mSurfaceView) + .setPreviewOrientation(mOrientation) + .setVideoQuality(mVideoQuality) + .setVideoEncoder(mVideoEncoder) + .setFlashEnabled(mFlash) + .setCamera(mCamera) + .setTimeToLive(mTimeToLive) + .setAudioEncoder(mAudioEncoder) + .setAudioQuality(mAudioQuality) + .setContext(mContext) + .setCallback(mCallback); + } + +} \ No newline at end of file diff --git a/src/main/java/net/majorkernelpanic/streaming/Stream.java b/src/main/java/net/majorkernelpanic/streaming/Stream.java new file mode 100644 index 00000000..57cd267a --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/Stream.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; + +/** + * An interface that represents a Stream. + */ +public interface Stream { + + /** + * Configures the stream. You need to call this before calling {@link #getSessionDescription()} + * to apply your configuration of the stream. + */ + public void configure() throws IllegalStateException, IOException; + + /** + * Starts the stream. + * This method can only be called after {@link Stream#configure()}. + */ + public void start() throws IllegalStateException, IOException; + + /** + * Stops the stream. + */ + public void stop(); + + /** + * Sets the Time To Live of packets sent over the network. + * @param ttl The time to live + * @throws IOException + */ + public void setTimeToLive(int ttl) throws IOException; + + /** + * Sets the destination ip address of the stream. + * @param dest The destination address of the stream + */ + public void setDestinationAddress(InetAddress dest); + + /** + * Sets the destination ports of the stream. + * If an odd number is supplied for the destination port then the next + * lower even number will be used for RTP and it will be used for RTCP. + * If an even number is supplied, it will be used for RTP and the next odd + * number will be used for RTCP. + * @param dport The destination port + */ + public void setDestinationPorts(int dport); + + /** + * Sets the destination ports of the stream. + * @param rtpPort Destination port that will be used for RTP + * @param rtcpPort Destination port that will be used for RTCP + */ + public void setDestinationPorts(int rtpPort, int rtcpPort); + + /** + * If a TCP is used as the transport protocol for the RTP session, + * the output stream to which RTP packets will be written to must + * be specified with this method. + */ + public void setOutputStream(OutputStream stream, byte channelIdentifier); + + /** + * Returns a pair of source ports, the first one is the + * one used for RTP and the second one is used for RTCP. + **/ + public int[] getLocalPorts(); + + /** + * Returns a pair of destination ports, the first one is the + * one used for RTP and the second one is used for RTCP. + **/ + public int[] getDestinationPorts(); + + + /** + * Returns the SSRC of the underlying {@link net.majorkernelpanic.streaming.rtp.RtpSocket}. + * @return the SSRC of the stream. + */ + public int getSSRC(); + + /** + * Returns an approximation of the bit rate consumed by the stream in bit per seconde. + */ + public long getBitrate(); + + /** + * Returns a description of the stream using SDP. + * This method can only be called after {@link Stream#configure()}. + * @throws IllegalStateException Thrown when {@link Stream#configure()} wa not called. + */ + public String getSessionDescription() throws IllegalStateException; + + public boolean isStreaming(); + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/audio/AACStream.java b/src/main/java/net/majorkernelpanic/streaming/audio/AACStream.java new file mode 100644 index 00000000..655cb803 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/audio/AACStream.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.audio; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.nio.ByteBuffer; + +import net.majorkernelpanic.streaming.SessionBuilder; +import net.majorkernelpanic.streaming.rtp.AACADTSPacketizer; +import net.majorkernelpanic.streaming.rtp.AACLATMPacketizer; +import net.majorkernelpanic.streaming.rtp.MediaCodecInputStream; +import android.annotation.SuppressLint; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.media.MediaRecorder; +import android.os.Build; +import android.os.Environment; +import android.service.textservice.SpellCheckerService.Session; +import android.util.Log; + +/** + * A class for streaming AAC from the camera of an android device using RTP. + * You should use a {@link Session} instantiated with {@link SessionBuilder} instead of using this class directly. + * Call {@link #setDestinationAddress(InetAddress)}, {@link #setDestinationPorts(int)} and {@link #setAudioQuality(AudioQuality)} + * to configure the stream. You can then call {@link #start()} to start the RTP stream. + * Call {@link #stop()} to stop the stream. + */ +public class AACStream extends AudioStream { + + public final static String TAG = "AACStream"; + + /** MPEG-4 Audio Object Types supported by ADTS. **/ + private static final String[] AUDIO_OBJECT_TYPES = { + "NULL", // 0 + "AAC Main", // 1 + "AAC LC (Low Complexity)", // 2 + "AAC SSR (Scalable Sample Rate)", // 3 + "AAC LTP (Long Term Prediction)" // 4 + }; + + /** There are 13 supported frequencies by ADTS. **/ + public static final int[] AUDIO_SAMPLING_RATES = { + 96000, // 0 + 88200, // 1 + 64000, // 2 + 48000, // 3 + 44100, // 4 + 32000, // 5 + 24000, // 6 + 22050, // 7 + 16000, // 8 + 12000, // 9 + 11025, // 10 + 8000, // 11 + 7350, // 12 + -1, // 13 + -1, // 14 + -1, // 15 + }; + + private String mSessionDescription = null; + private int mProfile, mSamplingRateIndex, mChannel, mConfig; + private SharedPreferences mSettings = null; + private AudioRecord mAudioRecord = null; + private Thread mThread = null; + + public AACStream() { + super(); + + if (!AACStreamingSupported()) { + Log.e(TAG,"AAC not supported on this phone"); + throw new RuntimeException("AAC not supported by this phone !"); + } else { + Log.d(TAG,"AAC supported on this phone"); + } + + } + + private static boolean AACStreamingSupported() { + if (Build.VERSION.SDK_INT<14) return false; + try { + MediaRecorder.OutputFormat.class.getField("AAC_ADTS"); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Some data (the actual sampling rate used by the phone and the AAC profile) needs to be stored once {@link #getSessionDescription()} is called. + * @param prefs The SharedPreferences that will be used to store the sampling rate + */ + public void setPreferences(SharedPreferences prefs) { + mSettings = prefs; + } + + @Override + public synchronized void start() throws IllegalStateException, IOException { + if (!mStreaming) { + configure(); + super.start(); + } + } + + public synchronized void configure() throws IllegalStateException, IOException { + super.configure(); + mQuality = mRequestedQuality.clone(); + + // Checks if the user has supplied an exotic sampling rate + int i=0; + for (;i12) mQuality.samplingRate = 16000; + + if (mMode != mRequestedMode || mPacketizer==null) { + mMode = mRequestedMode; + if (mMode == MODE_MEDIARECORDER_API) { + mPacketizer = new AACADTSPacketizer(); + } else { + mPacketizer = new AACLATMPacketizer(); + } + mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort); + mPacketizer.getRtpSocket().setOutputStream(mOutputStream, mChannelIdentifier); + } + + if (mMode == MODE_MEDIARECORDER_API) { + + testADTS(); + + // All the MIME types parameters used here are described in RFC 3640 + // SizeLength: 13 bits will be enough because ADTS uses 13 bits for frame length + // config: contains the object type + the sampling rate + the channel number + + // TODO: streamType always 5 ? profile-level-id always 15 ? + + mSessionDescription = "m=audio "+String.valueOf(getDestinationPorts()[0])+" RTP/AVP 96\r\n" + + "a=rtpmap:96 mpeg4-generic/"+mQuality.samplingRate+"\r\n"+ + "a=fmtp:96 streamtype=5; profile-level-id=15; mode=AAC-hbr; config="+Integer.toHexString(mConfig)+"; SizeLength=13; IndexLength=3; IndexDeltaLength=3;\r\n"; + + } else { + + mProfile = 2; // AAC LC + mChannel = 1; + mConfig = (mProfile & 0x1F) << 11 | (mSamplingRateIndex & 0x0F) << 7 | (mChannel & 0x0F) << 3; + + mSessionDescription = "m=audio "+String.valueOf(getDestinationPorts()[0])+" RTP/AVP 96\r\n" + + "a=rtpmap:96 mpeg4-generic/"+mQuality.samplingRate+"\r\n"+ + "a=fmtp:96 streamtype=5; profile-level-id=15; mode=AAC-hbr; config="+Integer.toHexString(mConfig)+"; SizeLength=13; IndexLength=3; IndexDeltaLength=3;\r\n"; + + } + + } + + @Override + protected void encodeWithMediaRecorder() throws IOException { + testADTS(); + ((AACADTSPacketizer)mPacketizer).setSamplingRate(mQuality.samplingRate); + super.encodeWithMediaRecorder(); + } + + @Override + @SuppressLint({ "InlinedApi", "NewApi" }) + protected void encodeWithMediaCodec() throws IOException { + + final int bufferSize = AudioRecord.getMinBufferSize(mQuality.samplingRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)*2; + + ((AACLATMPacketizer)mPacketizer).setSamplingRate(mQuality.samplingRate); + + mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, mQuality.samplingRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); + mMediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm"); + MediaFormat format = new MediaFormat(); + format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm"); + format.setInteger(MediaFormat.KEY_BIT_RATE, mQuality.bitRate); + format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); + format.setInteger(MediaFormat.KEY_SAMPLE_RATE, mQuality.samplingRate); + format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); + format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize); + mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + mAudioRecord.startRecording(); + mMediaCodec.start(); + + final MediaCodecInputStream inputStream = new MediaCodecInputStream(mMediaCodec); + final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); + + mThread = new Thread(new Runnable() { + @Override + public void run() { + int len = 0, bufferIndex = 0; + try { + while (!Thread.interrupted()) { + bufferIndex = mMediaCodec.dequeueInputBuffer(10000); + if (bufferIndex>=0) { + inputBuffers[bufferIndex].clear(); + len = mAudioRecord.read(inputBuffers[bufferIndex], bufferSize); + if (len == AudioRecord.ERROR_INVALID_OPERATION || len == AudioRecord.ERROR_BAD_VALUE) { + Log.e(TAG,"An error occured with the AudioRecord API !"); + } else { + //Log.v(TAG,"Pushing raw audio to the decoder: len="+len+" bs: "+inputBuffers[bufferIndex].capacity()); + mMediaCodec.queueInputBuffer(bufferIndex, 0, len, System.nanoTime()/1000, 0); + } + } + } + } catch (RuntimeException e) { + e.printStackTrace(); + } + } + }); + + mThread.start(); + + // The packetizer encapsulates this stream in an RTP stream and send it over the network + mPacketizer.setInputStream(inputStream); + mPacketizer.start(); + + mStreaming = true; + + } + + /** Stops the stream. */ + public synchronized void stop() { + if (mStreaming) { + if (mMode==MODE_MEDIACODEC_API) { + Log.d(TAG, "Interrupting threads..."); + mThread.interrupt(); + mAudioRecord.stop(); + mAudioRecord.release(); + mAudioRecord = null; + } + super.stop(); + } + } + + /** + * Returns a description of the stream using SDP. It can then be included in an SDP file. + * Will fail if called when streaming. + */ + public String getSessionDescription() throws IllegalStateException { + if (mSessionDescription == null) throw new IllegalStateException("You need to call configure() first !"); + return mSessionDescription; + } + + /** + * Records a short sample of AAC ADTS from the microphone to find out what the sampling rate really is + * On some phone indeed, no error will be reported if the sampling rate used differs from the + * one selected with setAudioSamplingRate + * @throws IOException + * @throws IllegalStateException + */ + @SuppressLint("InlinedApi") + private void testADTS() throws IllegalStateException, IOException { + + setAudioEncoder(MediaRecorder.AudioEncoder.AAC); + try { + Field name = MediaRecorder.OutputFormat.class.getField("AAC_ADTS"); + setOutputFormat(name.getInt(null)); + } + catch (Exception ignore) { + setOutputFormat(6); + } + + String key = PREF_PREFIX+"aac-"+mQuality.samplingRate; + + if (mSettings!=null) { + if (mSettings.contains(key)) { + String[] s = mSettings.getString(key, "").split(","); + mQuality.samplingRate = Integer.valueOf(s[0]); + mConfig = Integer.valueOf(s[1]); + mChannel = Integer.valueOf(s[2]); + return; + } + } + + final String TESTFILE = Environment.getExternalStorageDirectory().getPath()+"/spydroid-test.adts"; + + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + throw new IllegalStateException("No external storage or external storage not ready !"); + } + + // The structure of an ADTS packet is described here: http://wiki.multimedia.cx/index.php?title=ADTS + + // ADTS header is 7 or 9 bytes long + byte[] buffer = new byte[9]; + + mMediaRecorder = new MediaRecorder(); + mMediaRecorder.setAudioSource(mAudioSource); + mMediaRecorder.setOutputFormat(mOutputFormat); + mMediaRecorder.setAudioEncoder(mAudioEncoder); + mMediaRecorder.setAudioChannels(1); + mMediaRecorder.setAudioSamplingRate(mQuality.samplingRate); + mMediaRecorder.setAudioEncodingBitRate(mQuality.bitRate); + mMediaRecorder.setOutputFile(TESTFILE); + mMediaRecorder.setMaxDuration(1000); + mMediaRecorder.prepare(); + mMediaRecorder.start(); + + // We record for 1 sec + // TODO: use the MediaRecorder.OnInfoListener + try { + Thread.sleep(2000); + } catch (InterruptedException e) {} + + mMediaRecorder.stop(); + mMediaRecorder.release(); + mMediaRecorder = null; + + File file = new File(TESTFILE); + RandomAccessFile raf = new RandomAccessFile(file, "r"); + + // ADTS packets start with a sync word: 12bits set to 1 + while (true) { + if ( (raf.readByte()&0xFF) == 0xFF ) { + buffer[0] = raf.readByte(); + if ( (buffer[0]&0xF0) == 0xF0) break; + } + } + + raf.read(buffer,1,5); + + mSamplingRateIndex = (buffer[1]&0x3C)>>2 ; + mProfile = ( (buffer[1]&0xC0) >> 6 ) + 1 ; + mChannel = (buffer[1]&0x01) << 2 | (buffer[2]&0xC0) >> 6 ; + mQuality.samplingRate = AUDIO_SAMPLING_RATES[mSamplingRateIndex]; + + // 5 bits for the object type / 4 bits for the sampling rate / 4 bits for the channel / padding + mConfig = (mProfile & 0x1F) << 11 | (mSamplingRateIndex & 0x0F) << 7 | (mChannel & 0x0F) << 3; + + Log.i(TAG,"MPEG VERSION: " + ( (buffer[0]&0x08) >> 3 ) ); + Log.i(TAG,"PROTECTION: " + (buffer[0]&0x01) ); + Log.i(TAG,"PROFILE: " + AUDIO_OBJECT_TYPES[ mProfile ] ); + Log.i(TAG,"SAMPLING FREQUENCY: " + mQuality.samplingRate ); + Log.i(TAG,"CHANNEL: " + mChannel ); + + raf.close(); + + if (mSettings!=null) { + Editor editor = mSettings.edit(); + editor.putString(key, mQuality.samplingRate+","+mConfig+","+mChannel); + editor.commit(); + } + + if (!file.delete()) Log.e(TAG,"Temp file could not be erased"); + + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/audio/AMRNBStream.java b/src/main/java/net/majorkernelpanic/streaming/audio/AMRNBStream.java new file mode 100644 index 00000000..05f50ec7 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/audio/AMRNBStream.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.audio; + +import java.io.IOException; +import java.lang.reflect.Field; + +import net.majorkernelpanic.streaming.SessionBuilder; +import net.majorkernelpanic.streaming.rtp.AMRNBPacketizer; +import android.media.MediaRecorder; +import android.service.textservice.SpellCheckerService.Session; + +/** + * A class for streaming AAC from the camera of an android device using RTP. + * You should use a {@link Session} instantiated with {@link SessionBuilder} instead of using this class directly. + * Call {@link #setDestinationAddress(InetAddress)}, {@link #setDestinationPorts(int)} and {@link #setAudioQuality(AudioQuality)} + * to configure the stream. You can then call {@link #start()} to start the RTP stream. + * Call {@link #stop()} to stop the stream. + */ +public class AMRNBStream extends AudioStream { + + public AMRNBStream() { + super(); + + mPacketizer = new AMRNBPacketizer(); + + setAudioSource(MediaRecorder.AudioSource.CAMCORDER); + + try { + // RAW_AMR was deprecated in API level 16. + Field deprecatedName = MediaRecorder.OutputFormat.class.getField("RAW_AMR"); + setOutputFormat(deprecatedName.getInt(null)); + } catch (Exception e) { + setOutputFormat(MediaRecorder.OutputFormat.AMR_NB); + } + + setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); + + } + + /** + * Starts the stream. + */ + public synchronized void start() throws IllegalStateException, IOException { + if (!mStreaming) { + configure(); + super.start(); + } + } + + public synchronized void configure() throws IllegalStateException, IOException { + super.configure(); + mMode = MODE_MEDIARECORDER_API; + mQuality = mRequestedQuality.clone(); + } + + /** + * Returns a description of the stream using SDP. It can then be included in an SDP file. + */ + public String getSessionDescription() { + return "m=audio "+String.valueOf(getDestinationPorts()[0])+" RTP/AVP 96\r\n" + + "a=rtpmap:96 AMR/8000\r\n" + + "a=fmtp:96 octet-align=1;\r\n"; + } + + @Override + protected void encodeWithMediaCodec() throws IOException { + super.encodeWithMediaRecorder(); + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/audio/AudioQuality.java b/src/main/java/net/majorkernelpanic/streaming/audio/AudioQuality.java new file mode 100644 index 00000000..0fce0044 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/audio/AudioQuality.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.audio; + +/** + * A class that represents the quality of an audio stream. + */ +public class AudioQuality { + + /** Default audio stream quality. */ + public final static AudioQuality DEFAULT_AUDIO_QUALITY = new AudioQuality(8000,32000); + + /** Represents a quality for a video stream. */ + public AudioQuality() {} + + /** + * Represents a quality for an audio stream. + * @param samplingRate The sampling rate + * @param bitRate The bitrate in bit per seconds + */ + public AudioQuality(int samplingRate, int bitRate) { + this.samplingRate = samplingRate; + this.bitRate = bitRate; + } + + public int samplingRate = 0; + public int bitRate = 0; + + public boolean equals(AudioQuality quality) { + if (quality==null) return false; + return (quality.samplingRate == this.samplingRate & + quality.bitRate == this.bitRate); + } + + public AudioQuality clone() { + return new AudioQuality(samplingRate, bitRate); + } + + public static AudioQuality parseQuality(String str) { + AudioQuality quality = DEFAULT_AUDIO_QUALITY.clone(); + if (str != null) { + String[] config = str.split("-"); + try { + quality.bitRate = Integer.parseInt(config[0])*1000; // conversion to bit/s + quality.samplingRate = Integer.parseInt(config[1]); + } + catch (IndexOutOfBoundsException ignore) {} + } + return quality; + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/audio/AudioStream.java b/src/main/java/net/majorkernelpanic/streaming/audio/AudioStream.java new file mode 100644 index 00000000..9b3b2c90 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/audio/AudioStream.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.audio; + +import java.io.IOException; + +import net.majorkernelpanic.streaming.MediaStream; +import android.media.MediaRecorder; +import android.util.Log; + +/** + * Don't use this class directly. + */ +public abstract class AudioStream extends MediaStream { + + protected int mAudioSource; + protected int mOutputFormat; + protected int mAudioEncoder; + protected AudioQuality mRequestedQuality = AudioQuality.DEFAULT_AUDIO_QUALITY.clone(); + protected AudioQuality mQuality = mRequestedQuality.clone(); + + public AudioStream() { + setAudioSource(MediaRecorder.AudioSource.CAMCORDER); + } + + public void setAudioSource(int audioSource) { + mAudioSource = audioSource; + } + + public void setAudioQuality(AudioQuality quality) { + mRequestedQuality = quality; + } + + /** + * Returns the quality of the stream. + */ + public AudioQuality getAudioQuality() { + return mQuality; + } + + protected void setAudioEncoder(int audioEncoder) { + mAudioEncoder = audioEncoder; + } + + protected void setOutputFormat(int outputFormat) { + mOutputFormat = outputFormat; + } + + @Override + protected void encodeWithMediaRecorder() throws IOException { + + // We need a local socket to forward data output by the camera to the packetizer + createSockets(); + + Log.v(TAG,"Requested audio with "+mQuality.bitRate/1000+"kbps"+" at "+mQuality.samplingRate/1000+"kHz"); + + mMediaRecorder = new MediaRecorder(); + mMediaRecorder.setAudioSource(mAudioSource); + mMediaRecorder.setOutputFormat(mOutputFormat); + mMediaRecorder.setAudioEncoder(mAudioEncoder); + mMediaRecorder.setAudioChannels(1); + mMediaRecorder.setAudioSamplingRate(mQuality.samplingRate); + mMediaRecorder.setAudioEncodingBitRate(mQuality.bitRate); + + // We write the ouput of the camera in a local socket instead of a file ! + // This one little trick makes streaming feasible quiet simply: data from the camera + // can then be manipulated at the other end of the socket + mMediaRecorder.setOutputFile(mSender.getFileDescriptor()); + + mMediaRecorder.prepare(); + mMediaRecorder.start(); + + try { + // mReceiver.getInputStream contains the data from the camera + // the mPacketizer encapsulates this stream in an RTP stream and send it over the network + mPacketizer.setInputStream(mReceiver.getInputStream()); + mPacketizer.start(); + mStreaming = true; + } catch (IOException e) { + stop(); + throw new IOException("Something happened with the local sockets :/ Start failed !"); + } + + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/exceptions/CameraInUseException.java b/src/main/java/net/majorkernelpanic/streaming/exceptions/CameraInUseException.java new file mode 100644 index 00000000..f6d19229 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/exceptions/CameraInUseException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.exceptions; + +public class CameraInUseException extends RuntimeException { + + public CameraInUseException(String message) { + super(message); + } + + private static final long serialVersionUID = -1866132102949435675L; +} diff --git a/src/main/java/net/majorkernelpanic/streaming/exceptions/ConfNotSupportedException.java b/src/main/java/net/majorkernelpanic/streaming/exceptions/ConfNotSupportedException.java new file mode 100644 index 00000000..ffa8a113 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/exceptions/ConfNotSupportedException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.exceptions; + +public class ConfNotSupportedException extends RuntimeException { + + public ConfNotSupportedException(String message) { + super(message); + } + + private static final long serialVersionUID = 5876298277802827615L; +} diff --git a/src/main/java/net/majorkernelpanic/streaming/exceptions/InvalidSurfaceException.java b/src/main/java/net/majorkernelpanic/streaming/exceptions/InvalidSurfaceException.java new file mode 100644 index 00000000..45cd5b16 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/exceptions/InvalidSurfaceException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.exceptions; + +public class InvalidSurfaceException extends RuntimeException { + + private static final long serialVersionUID = -7238661340093544496L; + + public InvalidSurfaceException(String message) { + super(message); + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/exceptions/StorageUnavailableException.java b/src/main/java/net/majorkernelpanic/streaming/exceptions/StorageUnavailableException.java new file mode 100644 index 00000000..05742d41 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/exceptions/StorageUnavailableException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.exceptions; + +import java.io.IOException; + +public class StorageUnavailableException extends IOException { + + public StorageUnavailableException(String message) { + super(message); + } + + private static final long serialVersionUID = -7537890350373995089L; +} diff --git a/src/main/java/net/majorkernelpanic/streaming/gl/SurfaceManager.java b/src/main/java/net/majorkernelpanic/streaming/gl/SurfaceManager.java new file mode 100644 index 00000000..02bf54ac --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/gl/SurfaceManager.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* + * Based on the work of fadden + * + * Copyright 2012 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.majorkernelpanic.streaming.gl; + +import android.annotation.SuppressLint; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLExt; +import android.opengl.EGLSurface; +import android.opengl.GLES20; +import android.view.Surface; + +@SuppressLint("NewApi") +public class SurfaceManager { + + public final static String TAG = "TextureManager"; + + private static final int EGL_RECORDABLE_ANDROID = 0x3142; + + private EGLContext mEGLContext = null; + private EGLContext mEGLSharedContext = null; + private EGLSurface mEGLSurface = null; + private EGLDisplay mEGLDisplay = null; + + private Surface mSurface; + + /** + * Creates an EGL context and an EGL surface. + */ + public SurfaceManager(Surface surface, SurfaceManager manager) { + mSurface = surface; + mEGLSharedContext = manager.mEGLContext; + eglSetup(); + } + + /** + * Creates an EGL context and an EGL surface. + */ + public SurfaceManager(Surface surface) { + mSurface = surface; + eglSetup(); + } + + public void makeCurrent() { + if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) + throw new RuntimeException("eglMakeCurrent failed"); + } + + public void swapBuffer() { + EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface); + } + + /** + * Sends the presentation time stamp to EGL. Time is expressed in nanoseconds. + */ + public void setPresentationTime(long nsecs) { + EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs); + checkEglError("eglPresentationTimeANDROID"); + } + + /** + * Prepares EGL. We want a GLES 2.0 context and a surface that supports recording. + */ + private void eglSetup() { + mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { + throw new RuntimeException("unable to get EGL14 display"); + } + int[] version = new int[2]; + if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { + throw new RuntimeException("unable to initialize EGL14"); + } + + // Configure EGL for recording and OpenGL ES 2.0. + int[] attribList; + if (mEGLSharedContext == null) { + attribList = new int[] { + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL14.EGL_NONE + }; + } else { + attribList = new int[] { + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL_RECORDABLE_ANDROID, 1, + EGL14.EGL_NONE + }; + } + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, + numConfigs, 0); + checkEglError("eglCreateContext RGB888+recordable ES2"); + + // Configure context for OpenGL ES 2.0. + int[] attrib_list = { + EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, + EGL14.EGL_NONE + }; + + if (mEGLSharedContext == null) { + mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, attrib_list, 0); + } else { + mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], mEGLSharedContext, attrib_list, 0); + } + checkEglError("eglCreateContext"); + + // Create a window surface, and attach it to the Surface we received. + int[] surfaceAttribs = { + EGL14.EGL_NONE + }; + mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], mSurface, + surfaceAttribs, 0); + checkEglError("eglCreateWindowSurface"); + + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glDisable(GLES20.GL_CULL_FACE); + + } + + /** + * Discards all resources held by this class, notably the EGL context. Also releases the + * Surface that was passed to our constructor. + */ + public void release() { + if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { + EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, + EGL14.EGL_NO_CONTEXT); + EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface); + EGL14.eglDestroyContext(mEGLDisplay, mEGLContext); + EGL14.eglReleaseThread(); + EGL14.eglTerminate(mEGLDisplay); + } + mEGLDisplay = EGL14.EGL_NO_DISPLAY; + mEGLContext = EGL14.EGL_NO_CONTEXT; + mEGLSurface = EGL14.EGL_NO_SURFACE; + mSurface.release(); + } + + /** + * Checks for EGL errors. Throws an exception if one is found. + */ + private void checkEglError(String msg) { + int error; + if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) { + throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error)); + } + } + + + + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/gl/SurfaceView.java b/src/main/java/net/majorkernelpanic/streaming/gl/SurfaceView.java new file mode 100644 index 00000000..d78b1721 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/gl/SurfaceView.java @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.gl; + +import java.util.concurrent.Semaphore; + +import net.majorkernelpanic.streaming.MediaStream; +import net.majorkernelpanic.streaming.video.VideoStream; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.graphics.SurfaceTexture.OnFrameAvailableListener; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; + +/** + * An enhanced SurfaceView in which the camera preview will be rendered. + * This class was needed for two reasons.
+ * + * First, it allows to use to feed MediaCodec with the camera preview + * using the surface-to-buffer method while rendering it in a surface + * visible to the user. To force the surface-to-buffer method in + * libstreaming, call {@link MediaStream#setStreamingMethod(byte)} + * with {@link MediaStream#MODE_MEDIACODEC_API_2}.
+ * + * Second, it allows to force the aspect ratio of the SurfaceView + * to match the aspect ratio of the camera preview, so that the + * preview do not appear distorted to the user of your app. To do + * that, call {@link SurfaceView#setAspectRatioMode(int)} with + * {@link SurfaceView#ASPECT_RATIO_PREVIEW} after creating your + * {@link SurfaceView}.
+ * + */ +public class SurfaceView extends android.view.SurfaceView implements Runnable, OnFrameAvailableListener, SurfaceHolder.Callback { + + public final static String TAG = "SurfaceView"; + + /** + * The aspect ratio of the surface view will be equal + * to the aspect ration of the camera preview. + **/ + public static final int ASPECT_RATIO_PREVIEW = 0x01; + + /** The surface view will fill completely fill its parent. */ + public static final int ASPECT_RATIO_STRETCH = 0x00; + + private Thread mThread = null; + private Handler mHandler = null; + private boolean mFrameAvailable = false; + private boolean mRunning = true; + private int mAspectRatioMode = ASPECT_RATIO_STRETCH; + + // The surface in which the preview is rendered + private SurfaceManager mViewSurfaceManager = null; + + // The input surface of the MediaCodec + private SurfaceManager mCodecSurfaceManager = null; + + // Handles the rendering of the SurfaceTexture we got + // from the camera, onto a Surface + private TextureManager mTextureManager = null; + + private final Semaphore mLock = new Semaphore(0); + private final Object mSyncObject = new Object(); + + // Allows to force the aspect ratio of the preview + private ViewAspectRatioMeasurer mVARM = new ViewAspectRatioMeasurer(); + + public SurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + mHandler = new Handler(); + getHolder().addCallback(this); + } + + public void setAspectRatioMode(int mode) { + mAspectRatioMode = mode; + } + + public SurfaceTexture getSurfaceTexture() { + return mTextureManager.getSurfaceTexture(); + } + + public void addMediaCodecSurface(Surface surface) { + synchronized (mSyncObject) { + mCodecSurfaceManager = new SurfaceManager(surface,mViewSurfaceManager); + } + } + + public void removeMediaCodecSurface() { + synchronized (mSyncObject) { + if (mCodecSurfaceManager != null) { + mCodecSurfaceManager.release(); + mCodecSurfaceManager = null; + } + } + } + + public void startGLThread() { + Log.d(TAG,"Thread started."); + if (mTextureManager == null) { + mTextureManager = new TextureManager(); + } + if (mTextureManager.getSurfaceTexture() == null) { + mThread = new Thread(SurfaceView.this); + mRunning = true; + mThread.start(); + mLock.acquireUninterruptibly(); + } + } + + @Override + public void run() { + + mViewSurfaceManager = new SurfaceManager(getHolder().getSurface()); + mViewSurfaceManager.makeCurrent(); + mTextureManager.createTexture().setOnFrameAvailableListener(this); + + mLock.release(); + + try { + long ts = 0, oldts = 0; + while (mRunning) { + synchronized (mSyncObject) { + mSyncObject.wait(2500); + if (mFrameAvailable) { + mFrameAvailable = false; + + mViewSurfaceManager.makeCurrent(); + mTextureManager.updateFrame(); + mTextureManager.drawFrame(); + mViewSurfaceManager.swapBuffer(); + + if (mCodecSurfaceManager != null) { + mCodecSurfaceManager.makeCurrent(); + mTextureManager.drawFrame(); + oldts = ts; + ts = mTextureManager.getSurfaceTexture().getTimestamp(); + //Log.d(TAG,"FPS: "+(1000000000/(ts-oldts))); + mCodecSurfaceManager.setPresentationTime(ts); + mCodecSurfaceManager.swapBuffer(); + } + + } else { + Log.e(TAG,"No frame received !"); + } + } + } + } catch (InterruptedException ignore) { + } finally { + mViewSurfaceManager.release(); + mTextureManager.release(); + } + } + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + synchronized (mSyncObject) { + mFrameAvailable = true; + mSyncObject.notifyAll(); + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, + int height) { + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (mThread != null) { + mThread.interrupt(); + } + mRunning = false; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mVARM.getAspectRatio() > 0 && mAspectRatioMode == ASPECT_RATIO_PREVIEW) { + mVARM.measure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(mVARM.getMeasuredWidth(), mVARM.getMeasuredHeight()); + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + /** + * Requests a certain aspect ratio for the preview. You don't have to call this yourself, + * the {@link VideoStream} will do it when it's needed. + */ + public void requestAspectRatio(double aspectRatio) { + if (mVARM.getAspectRatio() != aspectRatio) { + mVARM.setAspectRatio(aspectRatio); + mHandler.post(new Runnable() { + @Override + public void run() { + if (mAspectRatioMode == ASPECT_RATIO_PREVIEW) { + requestLayout(); + } + } + }); + } + } + + /** + * This class is a helper to measure views that require a specific aspect ratio. + * @author Jesper Borgstrup + */ + public class ViewAspectRatioMeasurer { + + private double aspectRatio; + + public void setAspectRatio(double aspectRatio) { + this.aspectRatio = aspectRatio; + } + + public double getAspectRatio() { + return this.aspectRatio; + } + + /** + * Measure with the aspect ratio given at construction.
+ *
+ * After measuring, get the width and height with the {@link #getMeasuredWidth()} + * and {@link #getMeasuredHeight()} methods, respectively. + * @param widthMeasureSpec The width MeasureSpec passed in your View.onMeasure() method + * @param heightMeasureSpec The height MeasureSpec passed in your View.onMeasure() method + */ + public void measure(int widthMeasureSpec, int heightMeasureSpec) { + measure(widthMeasureSpec, heightMeasureSpec, this.aspectRatio); + } + + /** + * Measure with a specific aspect ratio
+ *
+ * After measuring, get the width and height with the {@link #getMeasuredWidth()} + * and {@link #getMeasuredHeight()} methods, respectively. + * @param widthMeasureSpec The width MeasureSpec passed in your View.onMeasure() method + * @param heightMeasureSpec The height MeasureSpec passed in your View.onMeasure() method + * @param aspectRatio The aspect ratio to calculate measurements in respect to + */ + public void measure(int widthMeasureSpec, int heightMeasureSpec, double aspectRatio) { + int widthMode = MeasureSpec.getMode( widthMeasureSpec ); + int widthSize = widthMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize( widthMeasureSpec ); + int heightMode = MeasureSpec.getMode( heightMeasureSpec ); + int heightSize = heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize( heightMeasureSpec ); + + if ( heightMode == MeasureSpec.EXACTLY && widthMode == MeasureSpec.EXACTLY ) { + /* + * Possibility 1: Both width and height fixed + */ + measuredWidth = widthSize; + measuredHeight = heightSize; + + } else if ( heightMode == MeasureSpec.EXACTLY ) { + /* + * Possibility 2: Width dynamic, height fixed + */ + measuredWidth = (int) Math.min( widthSize, heightSize * aspectRatio ); + measuredHeight = (int) (measuredWidth / aspectRatio); + + } else if ( widthMode == MeasureSpec.EXACTLY ) { + /* + * Possibility 3: Width fixed, height dynamic + */ + measuredHeight = (int) Math.min( heightSize, widthSize / aspectRatio ); + measuredWidth = (int) (measuredHeight * aspectRatio); + + } else { + /* + * Possibility 4: Both width and height dynamic + */ + if ( widthSize > heightSize * aspectRatio ) { + measuredHeight = heightSize; + measuredWidth = (int)( measuredHeight * aspectRatio ); + } else { + measuredWidth = widthSize; + measuredHeight = (int) (measuredWidth / aspectRatio); + } + + } + } + + private Integer measuredWidth = null; + /** + * Get the width measured in the latest call to measure(). + */ + public int getMeasuredWidth() { + if ( measuredWidth == null ) { + throw new IllegalStateException( "You need to run measure() before trying to get measured dimensions" ); + } + return measuredWidth; + } + + private Integer measuredHeight = null; + /** + * Get the height measured in the latest call to measure(). + */ + public int getMeasuredHeight() { + if ( measuredHeight == null ) { + throw new IllegalStateException( "You need to run measure() before trying to get measured dimensions" ); + } + return measuredHeight; + } + + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/gl/TextureManager.java b/src/main/java/net/majorkernelpanic/streaming/gl/TextureManager.java new file mode 100644 index 00000000..3e637e2c --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/gl/TextureManager.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* + * Based on the work of fadden + * + * Copyright 2012 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.majorkernelpanic.streaming.gl; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import android.annotation.SuppressLint; +import android.graphics.SurfaceTexture; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.opengl.Matrix; +import android.util.Log; + +/** + * Code for rendering a texture onto a surface using OpenGL ES 2.0. + */ +@SuppressLint("InlinedApi") +public class TextureManager { + + public final static String TAG = "TextureManager"; + + private static final int FLOAT_SIZE_BYTES = 4; + private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES; + private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0; + private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3; + private final float[] mTriangleVerticesData = { + // X, Y, Z, U, V + -1.0f, -1.0f, 0, 0.f, 0.f, + 1.0f, -1.0f, 0, 1.f, 0.f, + -1.0f, 1.0f, 0, 0.f, 1.f, + 1.0f, 1.0f, 0, 1.f, 1.f, + }; + + private FloatBuffer mTriangleVertices; + + private static final String VERTEX_SHADER = + "uniform mat4 uMVPMatrix;\n" + + "uniform mat4 uSTMatrix;\n" + + "attribute vec4 aPosition;\n" + + "attribute vec4 aTextureCoord;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " gl_Position = uMVPMatrix * aPosition;\n" + + " vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" + + "}\n"; + + private static final String FRAGMENT_SHADER = + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + // highp here doesn't seem to matter + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + + "}\n"; + + private float[] mMVPMatrix = new float[16]; + private float[] mSTMatrix = new float[16]; + + private int mProgram; + private int mTextureID = -12345; + private int muMVPMatrixHandle; + private int muSTMatrixHandle; + private int maPositionHandle; + private int maTextureHandle; + + private SurfaceTexture mSurfaceTexture; + + public TextureManager() { + mTriangleVertices = ByteBuffer.allocateDirect( + mTriangleVerticesData.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + mTriangleVertices.put(mTriangleVerticesData).position(0); + + Matrix.setIdentityM(mSTMatrix, 0); + } + + public int getTextureId() { + return mTextureID; + } + + public SurfaceTexture getSurfaceTexture() { + return mSurfaceTexture; + } + + public void updateFrame() { + mSurfaceTexture.updateTexImage(); + } + + public void drawFrame() { + checkGlError("onDrawFrame start"); + mSurfaceTexture.getTransformMatrix(mSTMatrix); + + //GLES20.glClearColor(0.0f, 1.0f, 0.0f, 1.0f); + //GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT); + + GLES20.glUseProgram(mProgram); + checkGlError("glUseProgram"); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); + + mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); + GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); + checkGlError("glVertexAttribPointer maPosition"); + GLES20.glEnableVertexAttribArray(maPositionHandle); + checkGlError("glEnableVertexAttribArray maPositionHandle"); + + mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET); + GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); + checkGlError("glVertexAttribPointer maTextureHandle"); + GLES20.glEnableVertexAttribArray(maTextureHandle); + checkGlError("glEnableVertexAttribArray maTextureHandle"); + + Matrix.setIdentityM(mMVPMatrix, 0); + GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0); + GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkGlError("glDrawArrays"); + GLES20.glFinish(); + } + + /** + * Initializes GL state. Call this after the EGL surface has been created and made current. + */ + public SurfaceTexture createTexture() { + mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER); + if (mProgram == 0) { + throw new RuntimeException("failed creating program"); + } + maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition"); + checkGlError("glGetAttribLocation aPosition"); + if (maPositionHandle == -1) { + throw new RuntimeException("Could not get attrib location for aPosition"); + } + maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord"); + checkGlError("glGetAttribLocation aTextureCoord"); + if (maTextureHandle == -1) { + throw new RuntimeException("Could not get attrib location for aTextureCoord"); + } + + muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); + checkGlError("glGetUniformLocation uMVPMatrix"); + if (muMVPMatrixHandle == -1) { + throw new RuntimeException("Could not get attrib location for uMVPMatrix"); + } + + muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix"); + checkGlError("glGetUniformLocation uSTMatrix"); + if (muSTMatrixHandle == -1) { + throw new RuntimeException("Could not get attrib location for uSTMatrix"); + } + + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + + mTextureID = textures[0]; + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); + checkGlError("glBindTexture mTextureID"); + + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_NEAREST); + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + checkGlError("glTexParameter"); + + mSurfaceTexture = new SurfaceTexture(mTextureID); + return mSurfaceTexture; + } + + public void release() { + mSurfaceTexture = null; + } + + /** + * Replaces the fragment shader. Pass in null to reset to default. + */ + public void changeFragmentShader(String fragmentShader) { + if (fragmentShader == null) { + fragmentShader = FRAGMENT_SHADER; + } + GLES20.glDeleteProgram(mProgram); + mProgram = createProgram(VERTEX_SHADER, fragmentShader); + if (mProgram == 0) { + throw new RuntimeException("failed creating program"); + } + } + + private int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + checkGlError("glCreateShader type=" + shaderType); + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + Log.e(TAG, "Could not compile shader " + shaderType + ":"); + Log.e(TAG, " " + GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + shader = 0; + } + return shader; + } + + private int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (pixelShader == 0) { + return 0; + } + + int program = GLES20.glCreateProgram(); + checkGlError("glCreateProgram"); + if (program == 0) { + Log.e(TAG, "Could not create program"); + } + GLES20.glAttachShader(program, vertexShader); + checkGlError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + checkGlError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + Log.e(TAG, "Could not link program: "); + Log.e(TAG, GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + program = 0; + } + return program; + } + + public void checkGlError(String op) { + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + Log.e(TAG, op + ": glError " + error); + throw new RuntimeException(op + ": glError " + error); + } + } +} diff --git a/src/main/java/net/majorkernelpanic/streaming/hw/CodecManager.java b/src/main/java/net/majorkernelpanic/streaming/hw/CodecManager.java new file mode 100644 index 00000000..8353b26e --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/hw/CodecManager.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.hw; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import android.annotation.SuppressLint; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.util.Log; + +@SuppressLint("InlinedApi") +public class CodecManager { + + public final static String TAG = "CodecManager"; + + public static final int[] SUPPORTED_COLOR_FORMATS = { + MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar, + MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar, + MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar, + MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar, + MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar + }; + + private static Codec[] sEncoders = null; + private static Codec[] sDecoders = null; + + static class Codec { + public Codec(String name, Integer[] formats) { + this.name = name; + this.formats = formats; + } + public String name; + public Integer[] formats; + } + + /** + * Lists all encoders that claim to support a color format that we know how to use. + * @return A list of those encoders + */ + @SuppressLint("NewApi") + public synchronized static Codec[] findEncodersForMimeType(String mimeType) { + if (sEncoders != null) return sEncoders; + + ArrayList encoders = new ArrayList(); + + // We loop through the encoders, apparently this can take up to a sec (testes on a GS3) + for(int j = MediaCodecList.getCodecCount() - 1; j >= 0; j--){ + MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(j); + if (!codecInfo.isEncoder()) continue; + + String[] types = codecInfo.getSupportedTypes(); + for (int i = 0; i < types.length; i++) { + if (types[i].equalsIgnoreCase(mimeType)) { + try { + MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType); + Set formats = new HashSet(); + + // And through the color formats supported + for (int k = 0; k < capabilities.colorFormats.length; k++) { + int format = capabilities.colorFormats[k]; + + for (int l=0;l decoders = new ArrayList(); + + // We loop through the decoders, apparently this can take up to a sec (testes on a GS3) + for(int j = MediaCodecList.getCodecCount() - 1; j >= 0; j--){ + MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(j); + if (codecInfo.isEncoder()) continue; + + String[] types = codecInfo.getSupportedTypes(); + for (int i = 0; i < types.length; i++) { + if (types[i].equalsIgnoreCase(mimeType)) { + try { + MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType); + Set formats = new HashSet(); + + // And through the color formats supported + for (int k = 0; k < capabilities.colorFormats.length; k++) { + int format = capabilities.colorFormats[k]; + + for (int l=0;l + * Feeding the encoder with a surface is not tested here. + * Some bugs you may have encountered:
+ *
    + *
  • U and V panes reversed
  • + *
  • Some padding is needed after the Y pane
  • + *
  • stride!=width or slice-height!=height
  • + *
+ */ +@SuppressLint("NewApi") +public class EncoderDebugger { + + public final static String TAG = "EncoderDebugger"; + + /** Prefix that will be used for all shared preferences saved by libstreaming. */ + private static final String PREF_PREFIX = "libstreaming-"; + + /** + * If this is set to false the test will be run only once and the result + * will be saved in the shared preferences. + */ + private static final boolean DEBUG = false; + + /** Set this to true to see more logs. */ + private static final boolean VERBOSE = false; + + /** Will be incremented every time this test is modified. */ + private static final int VERSION = 3; + + /** Bitrate that will be used with the encoder. */ + private final static int BITRATE = 1000000; + + /** Framerate that will be used to test the encoder. */ + private final static int FRAMERATE = 20; + + private final static String MIME_TYPE = "video/avc"; + + private final static int NB_DECODED = 34; + private final static int NB_ENCODED = 50; + + private int mDecoderColorFormat, mEncoderColorFormat; + private String mDecoderName, mEncoderName, mErrorLog; + private MediaCodec mEncoder, mDecoder; + private int mWidth, mHeight, mSize; + private byte[] mSPS, mPPS; + private byte[] mData, mInitialImage; + private MediaFormat mDecOutputFormat; + private NV21Convertor mNV21; + private SharedPreferences mPreferences; + private byte[][] mVideo, mDecodedVideo; + private String mB64PPS, mB64SPS; + + public synchronized static void asyncDebug(final Context context, final int width, final int height) { + new Thread(new Runnable() { + @Override + public void run() { + try { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + debug(prefs, width, height); + } catch (Exception e) {} + } + }).start(); + } + + public synchronized static EncoderDebugger debug(Context context, int width, int height) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return debug(prefs, width, height); + } + + public synchronized static EncoderDebugger debug(SharedPreferences prefs, int width, int height) { + EncoderDebugger debugger = new EncoderDebugger(prefs, width, height); + debugger.debug(); + return debugger; + } + + public String getB64PPS() { + return mB64PPS; + } + + public String getB64SPS() { + return mB64SPS; + } + + public String getEncoderName() { + return mEncoderName; + } + + public int getEncoderColorFormat() { + return mEncoderColorFormat; + } + + /** This {@link NV21Convertor} will do the necessary work to feed properly the encoder. */ + public NV21Convertor getNV21Convertor() { + return mNV21; + } + + /** A log of all the errors that occured during the test. */ + public String getErrorLog() { + return mErrorLog; + } + + private EncoderDebugger(SharedPreferences prefs, int width, int height) { + mPreferences = prefs; + mWidth = width; + mHeight = height; + mSize = width*height; + reset(); + } + + private void reset() { + mNV21 = new NV21Convertor(); + mVideo = new byte[NB_ENCODED][]; + mDecodedVideo = new byte[NB_DECODED][]; + mErrorLog = ""; + mPPS = null; + mSPS = null; + } + + private void debug() { + + // If testing the phone again is not needed, + // we just restore the result from the shared preferences + if (!checkTestNeeded()) { + String resolution = mWidth+"x"+mHeight+"-"; + + boolean success = mPreferences.getBoolean(PREF_PREFIX+resolution+"success",false); + if (!success) { + throw new RuntimeException("Phone not supported with this resolution ("+mWidth+"x"+mHeight+")"); + } + + mNV21.setSize(mWidth, mHeight); + mNV21.setSliceHeigth(mPreferences.getInt(PREF_PREFIX+resolution+"sliceHeight", 0)); + mNV21.setStride(mPreferences.getInt(PREF_PREFIX+resolution+"stride", 0)); + mNV21.setYPadding(mPreferences.getInt(PREF_PREFIX+resolution+"padding", 0)); + mNV21.setPlanar(mPreferences.getBoolean(PREF_PREFIX+resolution+"planar", false)); + mNV21.setColorPanesReversed(mPreferences.getBoolean(PREF_PREFIX+resolution+"reversed", false)); + mEncoderName = mPreferences.getString(PREF_PREFIX+resolution+"encoderName", ""); + mEncoderColorFormat = mPreferences.getInt(PREF_PREFIX+resolution+"colorFormat", 0); + mB64PPS = mPreferences.getString(PREF_PREFIX+resolution+"pps", ""); + mB64SPS = mPreferences.getString(PREF_PREFIX+resolution+"sps", ""); + + return; + } + + if (VERBOSE) Log.d(TAG, ">>>> Testing the phone for resolution "+mWidth+"x"+mHeight); + + // Builds a list of available encoders and decoders we may be able to use + // because they support some nice color formats + Codec[] encoders = CodecManager.findEncodersForMimeType(MIME_TYPE); + Codec[] decoders = CodecManager.findDecodersForMimeType(MIME_TYPE); + + int count = 0, n = 1; + for (int i=0;i> Test "+(n++)+"/"+count+": "+mEncoderName+" with color format "+mEncoderColorFormat+" at "+mWidth+"x"+mHeight); + + // Converts from NV21 to YUV420 with the specified parameters + mNV21.setSize(mWidth, mHeight); + mNV21.setSliceHeigth(mHeight); + mNV21.setStride(mWidth); + mNV21.setYPadding(0); + mNV21.setEncoderColorFormat(mEncoderColorFormat); + + // /!\ NV21Convertor can directly modify the input + createTestImage(); + mData = mNV21.convert(mInitialImage); + + try { + + // Starts the encoder + configureEncoder(); + searchSPSandPPS(); + + if (VERBOSE) Log.v(TAG, "SPS and PPS in b64: SPS="+mB64SPS+", PPS="+mB64PPS); + + // Feeds the encoder with an image repeatidly to produce some NAL units + encode(); + + // We now try to decode the NALs with decoders available on the phone + boolean decoded = false; + for (int k=0;k0) { + if (padding<4096) { + if (VERBOSE) Log.d(TAG, "Some padding is needed: "+padding); + mNV21.setYPadding(padding); + createTestImage(); + mData = mNV21.convert(mInitialImage); + encodeDecode(); + } else { + // TODO: try again with a different sliceHeight + // TODO: try again with the "slice-height" param + throw new RuntimeException("It is likely that sliceHeight!=height"); + } + } + + createTestImage(); + if (!compareChromaPanes(false)) { + if (compareChromaPanes(true)) { + mNV21.setColorPanesReversed(true); + if (VERBOSE) Log.d(TAG, "U and V pane are reversed"); + } else { + throw new RuntimeException("Incorrect U or V pane..."); + } + } + + saveTestResult(true); + Log.v(TAG, "The encoder "+mEncoderName+" is usable with resolution "+mWidth+"x"+mHeight); + return; + + } catch (Exception e) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); + String stack = sw.toString(); + String str = "Encoder "+mEncoderName+" cannot be used with color format "+mEncoderColorFormat; + if (VERBOSE) Log.e(TAG, str, e); + mErrorLog += str + "\n" + stack; + e.printStackTrace(); + } finally { + releaseEncoder(); + } + + } + } + + saveTestResult(false); + Log.e(TAG,"No usable encoder were found on the phone for resolution "+mWidth+"x"+mHeight); + throw new RuntimeException("No usable encoder were found on the phone for resolution "+mWidth+"x"+mHeight); + + } + + private boolean checkTestNeeded() { + String resolution = mWidth+"x"+mHeight+"-"; + + // Forces the test + if (DEBUG || mPreferences==null) return true; + + // If the sdk has changed on the phone, or the version of the test + // it has to be run again + if (mPreferences.contains(PREF_PREFIX+resolution+"lastSdk")) { + int lastSdk = mPreferences.getInt(PREF_PREFIX+resolution+"lastSdk", 0); + int lastVersion = mPreferences.getInt(PREF_PREFIX+resolution+"lastVersion", 0); + if (Build.VERSION.SDK_INT>lastSdk || VERSION>lastVersion) { + return true; + } + } else { + return true; + } + return false; + } + + + /** + * Saves the result of the test in the shared preferences, + * we will run it again only if the SDK has changed on the phone, + * or if this test has been modified. + */ + private void saveTestResult(boolean success) { + String resolution = mWidth+"x"+mHeight+"-"; + Editor editor = mPreferences.edit(); + + editor.putBoolean(PREF_PREFIX+resolution+"success", success); + + if (success) { + editor.putInt(PREF_PREFIX+resolution+"lastSdk", Build.VERSION.SDK_INT); + editor.putInt(PREF_PREFIX+resolution+"lastVersion", VERSION); + editor.putInt(PREF_PREFIX+resolution+"sliceHeight", mNV21.getSliceHeigth()); + editor.putInt(PREF_PREFIX+resolution+"stride", mNV21.getStride()); + editor.putInt(PREF_PREFIX+resolution+"padding", mNV21.getYPadding()); + editor.putBoolean(PREF_PREFIX+resolution+"planar", mNV21.getPlanar()); + editor.putBoolean(PREF_PREFIX+resolution+"reversed", mNV21.getUVPanesReversed()); + editor.putString(PREF_PREFIX+resolution+"encoderName", mEncoderName); + editor.putInt(PREF_PREFIX+resolution+"colorFormat", mEncoderColorFormat); + editor.putString(PREF_PREFIX+resolution+"encoderName", mEncoderName); + editor.putString(PREF_PREFIX+resolution+"pps", mB64PPS); + editor.putString(PREF_PREFIX+resolution+"sps", mB64SPS); + } + + editor.commit(); + } + + /** + * Creates the test image that will be used to feed the encoder. + */ + private void createTestImage() { + mInitialImage = new byte[3*mSize/2]; + for (int i=0;i50 && e>50) { + mDecodedVideo[j] = null; + f++; + break; + } + } + } + return f<=NB_DECODED/2; + } + + private int checkPaddingNeeded() { + int i = 0, j = 3*mSize/2-1, max = 0; + int[] r = new int[NB_DECODED]; + for (int k=0;k0) { + r[k] = ((i>>6)<<6); + max = r[k]>max ? r[k] : max; + if (VERBOSE) Log.e(TAG,"Padding needed: "+r[k]); + } else { + if (VERBOSE) Log.v(TAG,"No padding needed."); + } + } + } + + return ((max>>6)<<6); + } + + /** + * Compares the U or V pane of the initial image, and the U or V pane + * after having encoded & decoded the image. + */ + private boolean compareChromaPanes(boolean crossed) { + int d, f = 0; + + for (int j=0;j50) { + //if (VERBOSE) Log.e(TAG,"BUG "+(i-mSize)+" d "+d); + f++; + break; + } + } + + // We compare the V pane before with the U pane after + } else { + for (int i=mSize;i<3*mSize/2;i+=2) { + d = (mInitialImage[i]&0xFF) - (mDecodedVideo[j][i+1]&0xFF); + d = d<0 ? -d : d; + if (d>50) { + f++; + } + } + } + } + } + return f<=NB_DECODED/2; + } + + /** + * Converts the image obtained from the decoder to NV21. + */ + private void convertToNV21(int k) { + byte[] buffer = new byte[3*mSize/2]; + + int stride = mWidth, sliceHeight = mHeight; + int colorFormat = mDecoderColorFormat; + boolean planar = false; + + if (mDecOutputFormat != null) { + MediaFormat format = mDecOutputFormat; + if (format != null) { + if (format.containsKey("slice-height")) { + sliceHeight = format.getInteger("slice-height"); + if (sliceHeight0) { + colorFormat = format.getInteger(MediaFormat.KEY_COLOR_FORMAT); + } + } + } + } + + switch (colorFormat) { + case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: + case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar: + case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar: + planar = false; + break; + case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: + case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar: + planar = true; + break; + } + + for (int i=0;i=0) { + decInputBuffers[decInputIndex].clear(); + decInputBuffers[decInputIndex].put(prefix); + decInputBuffers[decInputIndex].put(mSPS); + mDecoder.queueInputBuffer(decInputIndex, 0, decInputBuffers[decInputIndex].position(), timestamp(), 0); + } else { + if (VERBOSE) Log.e(TAG,"No buffer available !"); + } + + decInputIndex = mDecoder.dequeueInputBuffer(1000000/FRAMERATE); + if (decInputIndex>=0) { + decInputBuffers[decInputIndex].clear(); + decInputBuffers[decInputIndex].put(prefix); + decInputBuffers[decInputIndex].put(mPPS); + mDecoder.queueInputBuffer(decInputIndex, 0, decInputBuffers[decInputIndex].position(), timestamp(), 0); + } else { + if (VERBOSE) Log.e(TAG,"No buffer available !"); + } + + + } + + private void releaseDecoder() { + if (mDecoder != null) { + try { + mDecoder.stop(); + } catch (Exception ignore) {} + try { + mDecoder.release(); + } catch (Exception ignore) {} + } + } + + /** + * Tries to obtain the SPS and the PPS for the encoder. + */ + private long searchSPSandPPS() { + + ByteBuffer[] inputBuffers = mEncoder.getInputBuffers(); + ByteBuffer[] outputBuffers = mEncoder.getOutputBuffers(); + BufferInfo info = new BufferInfo(); + byte[] csd = new byte[128]; + int len = 0, p = 4, q = 4; + long elapsed = 0, now = timestamp(); + + while (elapsed<3000000 && (mSPS==null || mPPS==null)) { + + // Some encoders won't give us the SPS and PPS unless they receive something to encode first... + int bufferIndex = mEncoder.dequeueInputBuffer(1000000/FRAMERATE); + if (bufferIndex>=0) { + check(inputBuffers[bufferIndex].capacity()>=mData.length, "The input buffer is not big enough."); + inputBuffers[bufferIndex].clear(); + inputBuffers[bufferIndex].put(mData, 0, mData.length); + mEncoder.queueInputBuffer(bufferIndex, 0, mData.length, timestamp(), 0); + } else { + if (VERBOSE) Log.e(TAG,"No buffer available !"); + } + + // We are looking for the SPS and the PPS here. As always, Android is very inconsistent, I have observed that some + // encoders will give those parameters through the MediaFormat object (that is the normal behaviour). + // But some other will not, in that case we try to find a NAL unit of type 7 or 8 in the byte stream outputed by the encoder... + + int index = mEncoder.dequeueOutputBuffer(info, 1000000/FRAMERATE); + + if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + + // The PPS and PPS shoud be there + MediaFormat format = mEncoder.getOutputFormat(); + ByteBuffer spsb = format.getByteBuffer("csd-0"); + ByteBuffer ppsb = format.getByteBuffer("csd-1"); + mSPS = new byte[spsb.capacity()-4]; + spsb.position(4); + spsb.get(mSPS,0,mSPS.length); + mPPS = new byte[ppsb.capacity()-4]; + ppsb.position(4); + ppsb.get(mPPS,0,mPPS.length); + break; + + } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + outputBuffers = mEncoder.getOutputBuffers(); + } else if (index>=0) { + + len = info.size; + if (len<128) { + outputBuffers[index].get(csd,0,len); + if (len>0 && csd[0]==0 && csd[1]==0 && csd[2]==0 && csd[3]==1) { + // Parses the SPS and PPS, they could be in two different packets and in a different order + //depending on the phone so we don't make any assumption about that + while (p=len) p=len; + if ((csd[q]&0x1F)==7) { + mSPS = new byte[p-q]; + System.arraycopy(csd, q, mSPS, 0, p-q); + } else { + mPPS = new byte[p-q]; + System.arraycopy(csd, q, mPPS, 0, p-q); + } + p += 4; + q = p; + } + } + } + mEncoder.releaseOutputBuffer(index, false); + } + + elapsed = timestamp() - now; + } + + check(mPPS != null & mSPS != null, "Could not determine the SPS & PPS."); + mB64PPS = Base64.encodeToString(mPPS, 0, mPPS.length, Base64.NO_WRAP); + mB64SPS = Base64.encodeToString(mSPS, 0, mSPS.length, Base64.NO_WRAP); + + return elapsed; + } + + private long encode() { + int n = 0; + long elapsed = 0, now = timestamp(); + int encOutputIndex = 0, encInputIndex = 0; + BufferInfo info = new BufferInfo(); + ByteBuffer[] encInputBuffers = mEncoder.getInputBuffers(); + ByteBuffer[] encOutputBuffers = mEncoder.getOutputBuffers(); + + while (elapsed<5000000) { + // Feeds the encoder with an image + encInputIndex = mEncoder.dequeueInputBuffer(1000000/FRAMERATE); + if (encInputIndex>=0) { + check(encInputBuffers[encInputIndex].capacity()>=mData.length, "The input buffer is not big enough."); + encInputBuffers[encInputIndex].clear(); + encInputBuffers[encInputIndex].put(mData, 0, mData.length); + mEncoder.queueInputBuffer(encInputIndex, 0, mData.length, timestamp(), 0); + } else { + if (VERBOSE) Log.d(TAG,"No buffer available !"); + } + + // Tries to get a NAL unit + encOutputIndex = mEncoder.dequeueOutputBuffer(info, 1000000/FRAMERATE); + if (encOutputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + encOutputBuffers = mEncoder.getOutputBuffers(); + } else if (encOutputIndex>=0) { + mVideo[n] = new byte[info.size]; + encOutputBuffers[encOutputIndex].clear(); + encOutputBuffers[encOutputIndex].get(mVideo[n++], 0, info.size); + mEncoder.releaseOutputBuffer(encOutputIndex, false); + if (n>=NB_ENCODED) { + flushMediaCodec(mEncoder); + return elapsed; + } + } + + elapsed = timestamp() - now; + } + + throw new RuntimeException("The encoder is too slow."); + + } + + /** + * @param withPrefix If set to true, the decoder will be fed with NALs preceeded with 0x00000001. + * @return How long it took to decode all the NALs + */ + private long decode(boolean withPrefix) { + int n = 0, i = 0, j = 0; + long elapsed = 0, now = timestamp(); + int decInputIndex = 0, decOutputIndex = 0; + ByteBuffer[] decInputBuffers = mDecoder.getInputBuffers(); + ByteBuffer[] decOutputBuffers = mDecoder.getOutputBuffers(); + BufferInfo info = new BufferInfo(); + + while (elapsed<3000000) { + + // Feeds the decoder with a NAL unit + if (i=0) { + int l1 = decInputBuffers[decInputIndex].capacity(); + int l2 = mVideo[i].length; + decInputBuffers[decInputIndex].clear(); + + if ((withPrefix && hasPrefix(mVideo[i])) || (!withPrefix && !hasPrefix(mVideo[i]))) { + check(l1>=l2, "The decoder input buffer is not big enough (nal="+l2+", capacity="+l1+")."); + decInputBuffers[decInputIndex].put(mVideo[i],0,mVideo[i].length); + } else if (withPrefix && !hasPrefix(mVideo[i])) { + check(l1>=l2+4, "The decoder input buffer is not big enough (nal="+(l2+4)+", capacity="+l1+")."); + decInputBuffers[decInputIndex].put(new byte[] {0,0,0,1}); + decInputBuffers[decInputIndex].put(mVideo[i],0,mVideo[i].length); + } else if (!withPrefix && hasPrefix(mVideo[i])) { + check(l1>=l2-4, "The decoder input buffer is not big enough (nal="+(l2-4)+", capacity="+l1+")."); + decInputBuffers[decInputIndex].put(mVideo[i],4,mVideo[i].length-4); + } + + mDecoder.queueInputBuffer(decInputIndex, 0, l2, timestamp(), 0); + i++; + } else { + if (VERBOSE) Log.d(TAG,"No buffer available !"); + } + } + + // Tries to get a decoded image + decOutputIndex = mDecoder.dequeueOutputBuffer(info, 1000000/FRAMERATE); + if (decOutputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + decOutputBuffers = mDecoder.getOutputBuffers(); + } else if (decOutputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + mDecOutputFormat = mDecoder.getOutputFormat(); + } else if (decOutputIndex>=0) { + if (n>2) { + // We have successfully encoded and decoded an image ! + int length = info.size; + mDecodedVideo[j] = new byte[length]; + decOutputBuffers[decOutputIndex].clear(); + decOutputBuffers[decOutputIndex].get(mDecodedVideo[j], 0, length); + // Converts the decoded frame to NV21 + convertToNV21(j); + if (j>=NB_DECODED-1) { + flushMediaCodec(mDecoder); + if (VERBOSE) Log.v(TAG, "Decoding "+n+" frames took "+elapsed/1000+" ms"); + return elapsed; + } + j++; + } + mDecoder.releaseOutputBuffer(decOutputIndex, false); + n++; + } + elapsed = timestamp() - now; + } + + throw new RuntimeException("The decoder did not decode anything."); + + } + + /** + * Makes sure the NAL has a header or not. + * @param withPrefix If set to true, the NAL will be preceeded with 0x00000001. + */ + private boolean hasPrefix(byte[] nal) { + if (nal[0] == 0 && nal[1] == 0 && nal[2] == 0 && nal[3] == 0x01) + return true; + else + return false; + } + + private void encodeDecode() { + encode(); + try { + configureDecoder(); + decode(true); + } finally { + releaseDecoder(); + } + } + + private void flushMediaCodec(MediaCodec mc) { + int index = 0; + BufferInfo info = new BufferInfo(); + while (index != MediaCodec.INFO_TRY_AGAIN_LATER) { + index = mc.dequeueOutputBuffer(info, 1000000/FRAMERATE); + if (index>=0) { + mc.releaseOutputBuffer(index, false); + } + } + } + + private void check(boolean cond, String message) { + if (!cond) { + if (VERBOSE) Log.e(TAG,message); + throw new IllegalStateException(message); + } + } + + private long timestamp() { + return System.nanoTime()/1000; + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/hw/NV21Convertor.java b/src/main/java/net/majorkernelpanic/streaming/hw/NV21Convertor.java new file mode 100644 index 00000000..f6cc81a5 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/hw/NV21Convertor.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.hw; + +import java.nio.ByteBuffer; + +import android.media.MediaCodecInfo; +import android.util.Log; + +/** + * Converts from NV21 to YUV420 semi planar or planar. + */ +public class NV21Convertor { + + private int mSliceHeight, mHeight; + private int mStride, mWidth; + private int mSize; + private boolean mPlanar, mPanesReversed = false; + private int mYPadding; + private byte[] mBuffer; + ByteBuffer mCopy; + + public void setSize(int width, int height) { + mHeight = height; + mWidth = width; + mSliceHeight = height; + mStride = width; + mSize = mWidth*mHeight; + } + + public void setStride(int width) { + mStride = width; + } + + public void setSliceHeigth(int height) { + mSliceHeight = height; + } + + public void setPlanar(boolean planar) { + mPlanar = planar; + } + + public void setYPadding(int padding) { + mYPadding = padding; + } + + public int getBufferSize() { + return 3*mSize/2; + } + + public void setEncoderColorFormat(int colorFormat) { + switch (colorFormat) { + case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: + case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar: + case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar: + setPlanar(false); + break; + case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: + case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar: + setPlanar(true); + break; + } + } + + public void setColorPanesReversed(boolean b) { + mPanesReversed = b; + } + + public int getStride() { + return mStride; + } + + public int getSliceHeigth() { + return mSliceHeight; + } + + public int getYPadding() { + return mYPadding; + } + + + public boolean getPlanar() { + return mPlanar; + } + + public boolean getUVPanesReversed() { + return mPanesReversed; + } + + public void convert(byte[] data, ByteBuffer buffer) { + byte[] result = convert(data); + int min = buffer.capacity() < data.length?buffer.capacity() : data.length; + buffer.put(result, 0, min); + } + + public byte[] convert(byte[] data) { + + // A buffer large enough for every case + if (mBuffer==null || mBuffer.length != 3*mSliceHeight*mStride/2+mYPadding) { + mBuffer = new byte[3*mSliceHeight*mStride/2+mYPadding]; + } + + if (!mPlanar) { + if (mSliceHeight==mHeight && mStride==mWidth) { + // Swaps U and V + if (!mPanesReversed) { + for (int i = mSize; i < mSize+mSize/2; i += 2) { + mBuffer[0] = data[i+1]; + data[i+1] = data[i]; + data[i] = mBuffer[0]; + } + } + if (mYPadding>0) { + System.arraycopy(data, 0, mBuffer, 0, mSize); + System.arraycopy(data, mSize, mBuffer, mSize+mYPadding, mSize/2); + return mBuffer; + } + return data; + } + } else { + if (mSliceHeight==mHeight && mStride==mWidth) { + // De-interleave U and V + if (!mPanesReversed) { + for (int i = 0; i < mSize/4; i+=1) { + mBuffer[i] = data[mSize+2*i+1]; + mBuffer[mSize/4+i] = data[mSize+2*i]; + } + } else { + for (int i = 0; i < mSize/4; i+=1) { + mBuffer[i] = data[mSize+2*i]; + mBuffer[mSize/4+i] = data[mSize+2*i+1]; + } + } + if (mYPadding == 0) { + System.arraycopy(mBuffer, 0, data, mSize, mSize/2); + } else { + System.arraycopy(data, 0, mBuffer, 0, mSize); + System.arraycopy(mBuffer, 0, mBuffer, mSize+mYPadding, mSize/2); + return mBuffer; + } + return data; + } + } + + return data; + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/mp4/MP4Config.java b/src/main/java/net/majorkernelpanic/streaming/mp4/MP4Config.java new file mode 100644 index 00000000..3457ce6e --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/mp4/MP4Config.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.mp4; +import java.io.FileNotFoundException; +import java.io.IOException; + +import android.util.Base64; +import android.util.Log; + +/** + * Finds SPS & PPS parameters in mp4 file. + */ +public class MP4Config { + + public final static String TAG = "MP4Config"; + + private MP4Parser mp4Parser; + private String mProfilLevel, mPPS, mSPS; + + public MP4Config(String profil, String sps, String pps) { + mProfilLevel = profil; + mPPS = pps; + mSPS = sps; + } + + public MP4Config(String sps, String pps) { + mPPS = pps; + mSPS = sps; + mProfilLevel = MP4Parser.toHexString(Base64.decode(sps, Base64.NO_WRAP),1,3); + } + + public MP4Config(byte[] sps, byte[] pps) { + mPPS = Base64.encodeToString(pps, 0, pps.length, Base64.NO_WRAP); + mSPS = Base64.encodeToString(sps, 0, sps.length, Base64.NO_WRAP); + mProfilLevel = MP4Parser.toHexString(sps,1,3); + } + + /** + * Finds SPS & PPS parameters inside a .mp4. + * @param path Path to the file to analyze + * @throws IOException + * @throws FileNotFoundException + */ + public MP4Config (String path) throws IOException, FileNotFoundException { + + StsdBox stsdBox; + + // We open the mp4 file and parse it + try { + mp4Parser = MP4Parser.parse(path); + } catch (IOException ignore) { + // Maybe enough of the file has been parsed and we can get the stsd box + } + + // We find the stsdBox + stsdBox = mp4Parser.getStsdBox(); + mPPS = stsdBox.getB64PPS(); + mSPS = stsdBox.getB64SPS(); + mProfilLevel = stsdBox.getProfileLevel(); + + mp4Parser.close(); + + } + + public String getProfileLevel() { + return mProfilLevel; + } + + public String getB64PPS() { + Log.d(TAG, "PPS: "+mPPS); + return mPPS; + } + + public String getB64SPS() { + Log.d(TAG, "SPS: "+mSPS); + return mSPS; + } + +} \ No newline at end of file diff --git a/src/main/java/net/majorkernelpanic/streaming/mp4/MP4Parser.java b/src/main/java/net/majorkernelpanic/streaming/mp4/MP4Parser.java new file mode 100644 index 00000000..54ad215e --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/mp4/MP4Parser.java @@ -0,0 +1,255 @@ +package net.majorkernelpanic.streaming.mp4; +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.util.HashMap; + +import android.util.Base64; +import android.util.Log; + +/** + * Parse an mp4 file. + * An mp4 file contains a tree where each node has a name and a size. + * This class is used by H264Stream.java to determine the SPS and PPS parameters of a short video recorded by the phone. + */ +public class MP4Parser { + + private static final String TAG = "MP4Parser"; + + private HashMap mBoxes = new HashMap(); + private final RandomAccessFile mFile; + private long mPos = 0; + + + /** Parses the mp4 file. **/ + public static MP4Parser parse(String path) throws IOException { + return new MP4Parser(path); + } + + private MP4Parser(final String path) throws IOException, FileNotFoundException { + mFile = new RandomAccessFile(new File(path), "r"); + try { + parse("",mFile.length()); + } catch (Exception e) { + e.printStackTrace(); + throw new IOException("Parse error: malformed mp4 file"); + } + } + + public void close() { + try { + mFile.close(); + } catch (Exception e) {}; + } + + public long getBoxPos(String box) throws IOException { + Long r = mBoxes.get(box); + + if (r==null) throw new IOException("Box not found: "+box); + return mBoxes.get(box); + } + + public StsdBox getStsdBox() throws IOException { + try { + return new StsdBox(mFile,getBoxPos("/moov/trak/mdia/minf/stbl/stsd")); + } catch (IOException e) { + throw new IOException("stsd box could not be found"); + } + } + + private void parse(String path, long len) throws IOException { + ByteBuffer byteBuffer; + long sum = 0, newlen = 0; + byte[] buffer = new byte[8]; + String name = ""; + + if(!path.equals("")) mBoxes.put(path, mPos-8); + + while (sum name: "+name+" position: "+mPos+", length: "+newlen); + sum += newlen; + parse(path+'/'+name,newlen); + + } + else { + if( len < 8){ + mFile.seek(mFile.getFilePointer() - 8 + len); + sum += len-8; + } else { + int skipped = mFile.skipBytes((int)(len-8)); + if (skipped < ((int)(len-8))) { + throw new IOException(); + } + mPos += len-8; + sum += len-8; + } + } + } + } + + private boolean validBoxName(byte[] buffer) { + for (int i=0;i<4;i++) { + // If the next 4 bytes are neither lowercase letters nor numbers + if ((buffer[i+4]< 'a' || buffer[i+4]>'z') && (buffer[i+4]<'0'|| buffer[i+4]>'9') ) return false; + } + return true; + } + + static String toHexString(byte[] buffer,int start, int len) { + String c; + StringBuilder s = new StringBuilder(); + for (int i=start;i + * aligned(8) class AVCDecoderConfigurationRecord { + * unsigned int(8) configurationVersion = 1; + * unsigned int(8) AVCProfileIndication; + * unsigned int(8) profile_compatibility; + * unsigned int(8) AVCLevelIndication; + * bit(6) reserved = ‘111111’b; + * unsigned int(2) lengthSizeMinusOne; + * bit(3) reserved = ‘111’b; + * unsigned int(5) numOfSequenceParameterSets; + * for (i=0; i< numOfSequenceParameterSets; i++) { + * unsigned int(16) sequenceParameterSetLength ; + * bit(8*sequenceParameterSetLength) sequenceParameterSetNALUnit; + * } + * unsigned int(8) numOfPictureParameterSets; + * for (i=0; i< numOfPictureParameterSets; i++) { + * unsigned int(16) pictureParameterSetLength; + * bit(8*pictureParameterSetLength) pictureParameterSetNALUnit; + * } + * } + * + */ + try { + + // TODO: Here we assume that numOfSequenceParameterSets = 1, numOfPictureParameterSets = 1 ! + // Here we extract the SPS parameter + fis.skipBytes(7); + spsLength = 0xFF&fis.readByte(); + sps = new byte[spsLength]; + fis.read(sps,0,spsLength); + // Here we extract the PPS parameter + fis.skipBytes(2); + ppsLength = 0xFF&fis.readByte(); + pps = new byte[ppsLength]; + fis.read(pps,0,ppsLength); + + } catch (IOException e) { + return false; + } + + return true; + } + + private boolean findBoxAvcc() { + try { + fis.seek(pos+8); + while (true) { + while (fis.read() != 'a'); + fis.read(buffer,0,3); + if (buffer[0] == 'v' && buffer[1] == 'c' && buffer[2] == 'C') break; + } + } catch (IOException e) { + return false; + } + return true; + + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/rtcp/SenderReport.java b/src/main/java/net/majorkernelpanic/streaming/rtcp/SenderReport.java new file mode 100644 index 00000000..7f91be04 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/rtcp/SenderReport.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.rtcp; + +import static net.majorkernelpanic.streaming.rtp.RtpSocket.TRANSPORT_TCP; +import static net.majorkernelpanic.streaming.rtp.RtpSocket.TRANSPORT_UDP; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.nio.channels.IllegalSelectorException; + +import android.os.SystemClock; +import android.util.Log; + +/** + * Implementation of Sender Report RTCP packets. + */ +public class SenderReport { + + public static final int MTU = 1500; + + private static final int PACKET_LENGTH = 28; + + private MulticastSocket usock; + private DatagramPacket upack; + + private int mTransport; + private OutputStream mOutputStream = null; + private byte[] mBuffer = new byte[MTU]; + private int mSSRC, mPort = -1; + private int mOctetCount = 0, mPacketCount = 0; + private long interval, delta, now, oldnow; + private byte mTcpHeader[]; + + public SenderReport(int ssrc) throws IOException { + super(); + this.mSSRC = ssrc; + } + + public SenderReport() { + + mTransport = TRANSPORT_UDP; + mTcpHeader = new byte[] {'$',0,0,PACKET_LENGTH}; + + /* Version(2) Padding(0) */ + /* ^ ^ PT = 0 */ + /* | | ^ */ + /* | -------- | */ + /* | |--------------------- */ + /* | || */ + /* | || */ + mBuffer[0] = (byte) Integer.parseInt("10000000",2); + + /* Packet Type PT */ + mBuffer[1] = (byte) 200; + + /* Byte 2,3 -> Length */ + setLong(PACKET_LENGTH/4-1, 2, 4); + + /* Byte 4,5,6,7 -> SSRC */ + /* Byte 8,9,10,11 -> NTP timestamp hb */ + /* Byte 12,13,14,15 -> NTP timestamp lb */ + /* Byte 16,17,18,19 -> RTP timestamp */ + /* Byte 20,21,22,23 -> packet count */ + /* Byte 24,25,26,27 -> octet count */ + + try { + usock = new MulticastSocket(); + } catch (IOException e) { + // Very unlikely to happen. Means that all UDP ports are already being used + throw new RuntimeException(e.getMessage()); + } + upack = new DatagramPacket(mBuffer, 1); + + // By default we sent one report every 3 secconde + interval = 3000; + + } + + public void close() { + usock.close(); + } + + /** + * Sets the temporal interval between two RTCP Sender Reports. + * Default interval is set to 3 seconds. + * Set 0 to disable RTCP. + * @param interval The interval in milliseconds + */ + public void setInterval(long interval) { + this.interval = interval; + } + + /** + * Updates the number of packets sent, and the total amount of data sent. + * @param length The length of the packet + * @param rtpts + * The RTP timestamp. + * @throws IOException + **/ + public void update(int length, long rtpts) throws IOException { + mPacketCount += 1; + mOctetCount += length; + setLong(mPacketCount, 20, 24); + setLong(mOctetCount, 24, 28); + + now = SystemClock.elapsedRealtime(); + delta += oldnow != 0 ? now-oldnow : 0; + oldnow = now; + if (interval>0) { + if (delta>=interval) { + // We send a Sender Report + send(System.nanoTime(), rtpts); + delta = 0; + } + } + + } + + public void setSSRC(int ssrc) { + this.mSSRC = ssrc; + setLong(ssrc,4,8); + mPacketCount = 0; + mOctetCount = 0; + setLong(mPacketCount, 20, 24); + setLong(mOctetCount, 24, 28); + } + + public void setDestination(InetAddress dest, int dport) { + mTransport = TRANSPORT_UDP; + mPort = dport; + upack.setPort(dport); + upack.setAddress(dest); + } + + /** + * If a TCP is used as the transport protocol for the RTP session, + * the output stream to which RTP packets will be written to must + * be specified with this method. + */ + public void setOutputStream(OutputStream os, byte channelIdentifier) { + mTransport = TRANSPORT_TCP; + mOutputStream = os; + mTcpHeader[1] = channelIdentifier; + } + + public int getPort() { + return mPort; + } + + public int getLocalPort() { + return usock.getLocalPort(); + } + + public int getSSRC() { + return mSSRC; + } + + /** + * Resets the reports (total number of bytes sent, number of packets sent, etc.) + */ + public void reset() { + mPacketCount = 0; + mOctetCount = 0; + setLong(mPacketCount, 20, 24); + setLong(mOctetCount, 24, 28); + delta = now = oldnow = 0; + } + + private void setLong(long n, int begin, int end) { + for (end--; end >= begin; end--) { + mBuffer[end] = (byte) (n % 256); + n >>= 8; + } + } + + /** + * Sends the RTCP packet over the network. + * + * @param ntpts + * the NTP timestamp. + * @param rtpts + * the RTP timestamp. + */ + private void send(long ntpts, long rtpts) throws IOException { + long hb = ntpts/1000000000; + long lb = ( ( ntpts - hb*1000000000 ) * 4294967296L )/1000000000; + setLong(hb, 8, 12); + setLong(lb, 12, 16); + setLong(rtpts, 16, 20); + if (mTransport == TRANSPORT_UDP) { + upack.setLength(PACKET_LENGTH); + usock.send(upack); + } else { + synchronized (mOutputStream) { + try { + mOutputStream.write(mTcpHeader); + mOutputStream.write(mBuffer, 0, PACKET_LENGTH); + } catch (Exception e) {} + } + } + } + + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/rtp/AACADTSPacketizer.java b/src/main/java/net/majorkernelpanic/streaming/rtp/AACADTSPacketizer.java new file mode 100644 index 00000000..bbff5861 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/rtp/AACADTSPacketizer.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.rtp; + +import java.io.IOException; + +import net.majorkernelpanic.streaming.audio.AACStream; +import android.os.SystemClock; +import android.util.Log; + +/** + * + * RFC 3640. + * + * This packetizer must be fed with an InputStream containing ADTS AAC. + * AAC will basically be rewrapped in an RTP stream and sent over the network. + * This packetizer only implements the aac-hbr mode (High Bit-rate AAC) and + * each packet only carry a single and complete AAC access unit. + * + */ +public class AACADTSPacketizer extends AbstractPacketizer implements Runnable { + + private final static String TAG = "AACADTSPacketizer"; + + private Thread t; + private int samplingRate = 8000; + + public AACADTSPacketizer() { + super(); + } + + public void start() { + if (t==null) { + t = new Thread(this); + t.start(); + } + } + + public void stop() { + if (t != null) { + try { + is.close(); + } catch (IOException ignore) {} + t.interrupt(); + try { + t.join(); + } catch (InterruptedException e) {} + t = null; + } + } + + public void setSamplingRate(int samplingRate) { + this.samplingRate = samplingRate; + socket.setClockFrequency(samplingRate); + } + + public void run() { + + Log.d(TAG,"AAC ADTS packetizer started !"); + + // "A packet SHALL carry either one or more complete Access Units, or a + // single fragment of an Access Unit. Fragments of the same Access Unit + // have the same time stamp but different RTP sequence numbers. The + // marker bit in the RTP header is 1 on the last fragment of an Access + // Unit, and 0 on all other fragments." RFC 3640 + + // ADTS header fields that we need to parse + boolean protection; + int frameLength, sum, length, nbau, nbpk, samplingRateIndex, profile; + long oldtime = SystemClock.elapsedRealtime(), now = oldtime; + byte[] header = new byte[8]; + + try { + while (!Thread.interrupted()) { + + // Synchronisation: ADTS packet starts with 12bits set to 1 + while (true) { + if ( (is.read()&0xFF) == 0xFF ) { + header[1] = (byte) is.read(); + if ( (header[1]&0xF0) == 0xF0) break; + } + } + + // Parse adts header (ADTS packets start with a 7 or 9 byte long header) + fill(header, 2, 5); + + // The protection bit indicates whether or not the header contains the two extra bytes + protection = (header[1]&0x01)>0 ? true : false; + frameLength = (header[3]&0x03) << 11 | + (header[4]&0xFF) << 3 | + (header[5]&0xFF) >> 5 ; + frameLength -= (protection ? 7 : 9); + + // Number of AAC frames in the ADTS frame + nbau = (header[6]&0x03) + 1; + + // The number of RTP packets that will be sent for this ADTS frame + nbpk = frameLength/MAXPACKETSIZE + 1; + + // Read CRS if any + if (!protection) is.read(header,0,2); + + samplingRate = AACStream.AUDIO_SAMPLING_RATES[(header[2]&0x3C) >> 2]; + profile = ( (header[2]&0xC0) >> 6 ) + 1 ; + + // We update the RTP timestamp + ts += 1024L*1000000000L/samplingRate; //stats.average(); + + //Log.d(TAG,"frameLength: "+frameLength+" protection: "+protection+" p: "+profile+" sr: "+samplingRate); + + sum = 0; + while (sum MAXPACKETSIZE-rtphl-4) { + length = MAXPACKETSIZE-rtphl-4; + } + else { + length = frameLength-sum; + socket.markNextPacket(); + } + sum += length; + fill(buffer, rtphl+4, length); + + // AU-headers-length field: contains the size in bits of a AU-header + // 13+3 = 16 bits -> 13bits for AU-size and 3bits for AU-Index / AU-Index-delta + // 13 bits will be enough because ADTS uses 13 bits for frame length + buffer[rtphl] = 0; + buffer[rtphl+1] = 0x10; + + // AU-size + buffer[rtphl+2] = (byte) (frameLength>>5); + buffer[rtphl+3] = (byte) (frameLength<<3); + + // AU-Index + buffer[rtphl+3] &= 0xF8; + buffer[rtphl+3] |= 0x00; + + send(rtphl+4+length); + + } + + } + } catch (IOException e) { + // Ignore + } catch (ArrayIndexOutOfBoundsException e) { + Log.e(TAG,"ArrayIndexOutOfBoundsException: "+(e.getMessage()!=null?e.getMessage():"unknown error")); + e.printStackTrace(); + } catch (InterruptedException ignore) {} + + Log.d(TAG,"AAC ADTS packetizer stopped !"); + + } + + private int fill(byte[] buffer, int offset,int length) throws IOException { + int sum = 0, len; + while (sum0) { + + bufferInfo = ((MediaCodecInputStream)is).getLastBufferInfo(); + //Log.d(TAG,"length: "+length+" ts: "+bufferInfo.presentationTimeUs); + oldts = ts; + ts = bufferInfo.presentationTimeUs*1000; + + // Seems to happen sometimes + if (oldts>ts) { + socket.commitBuffer(); + continue; + } + + socket.markNextPacket(); + socket.updateTimestamp(ts); + + // AU-headers-length field: contains the size in bits of a AU-header + // 13+3 = 16 bits -> 13bits for AU-size and 3bits for AU-Index / AU-Index-delta + // 13 bits will be enough because ADTS uses 13 bits for frame length + buffer[rtphl] = 0; + buffer[rtphl+1] = 0x10; + + // AU-size + buffer[rtphl+2] = (byte) (length>>5); + buffer[rtphl+3] = (byte) (length<<3); + + // AU-Index + buffer[rtphl+3] &= 0xF8; + buffer[rtphl+3] |= 0x00; + + send(rtphl+length+4); + + } else { + socket.commitBuffer(); + } + + } + } catch (IOException e) { + } catch (ArrayIndexOutOfBoundsException e) { + Log.e(TAG,"ArrayIndexOutOfBoundsException: "+(e.getMessage()!=null?e.getMessage():"unknown error")); + e.printStackTrace(); + } catch (InterruptedException ignore) {} + + Log.d(TAG,"AAC LATM packetizer stopped !"); + + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/rtp/AMRNBPacketizer.java b/src/main/java/net/majorkernelpanic/streaming/rtp/AMRNBPacketizer.java new file mode 100644 index 00000000..5ad14639 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/rtp/AMRNBPacketizer.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.rtp; + +import java.io.IOException; + +import android.util.Log; + +/** + * + * RFC 3267. + * + * AMR Streaming over RTP. + * + * Must be fed with an InputStream containing raw AMR NB + * Stream must begin with a 6 bytes long header: "#!AMR\n", it will be skipped + * + */ +public class AMRNBPacketizer extends AbstractPacketizer implements Runnable { + + public final static String TAG = "AMRNBPacketizer"; + + private final int AMR_HEADER_LENGTH = 6; // "#!AMR\n" + private static final int AMR_FRAME_HEADER_LENGTH = 1; // Each frame has a short header + private static final int[] sFrameBits = {95, 103, 118, 134, 148, 159, 204, 244}; + private int samplingRate = 8000; + + private Thread t; + + public AMRNBPacketizer() { + super(); + socket.setClockFrequency(samplingRate); + } + + public void start() { + if (t==null) { + t = new Thread(this); + t.start(); + } + } + + public void stop() { + if (t != null) { + try { + is.close(); + } catch (IOException ignore) {} + t.interrupt(); + try { + t.join(); + } catch (InterruptedException e) {} + t = null; + } + } + + public void run() { + + int frameLength, frameType; + long now = System.nanoTime(), oldtime = now; + byte[] header = new byte[AMR_HEADER_LENGTH]; + + try { + + // Skip raw AMR header + fill(header,0,AMR_HEADER_LENGTH); + + if (header[5] != '\n') { + Log.e(TAG,"Bad header ! AMR not correcty supported by the phone !"); + return; + } + + while (!Thread.interrupted()) { + + buffer = socket.requestBuffer(); + buffer[rtphl] = (byte) 0xF0; + + // First we read the frame header + fill(buffer, rtphl+1,AMR_FRAME_HEADER_LENGTH); + + // Then we calculate the frame payload length + frameType = (Math.abs(buffer[rtphl + 1]) >> 3) & 0x0f; + frameLength = (sFrameBits[frameType]+7)/8; + + // And we read the payload + fill(buffer, rtphl+2,frameLength); + + //Log.d(TAG,"Frame length: "+frameLength+" frameType: "+frameType); + + // RFC 3267 Page 14: "For AMR, the sampling frequency is 8 kHz" + // FIXME: Is this really always the case ?? + ts += 160L*1000000000L/samplingRate; //stats.average(); + socket.updateTimestamp(ts); + socket.markNextPacket(); + + //Log.d(TAG,"expected: "+ expected + " measured: "+measured); + + send(rtphl+1+AMR_FRAME_HEADER_LENGTH+frameLength); + + } + + } catch (IOException e) { + } catch (InterruptedException e) {} + + Log.d(TAG,"AMR packetizer stopped !"); + + } + + private int fill(byte[] buffer, int offset,int length) throws IOException { + int sum = 0, len; + while (sumperiod) { + elapsed = 0; + long now = System.nanoTime(); + if (!initoffset || (now - start < 0)) { + start = now; + duration = 0; + initoffset = true; + } + // Prevents drifting issues by comparing the real duration of the + // stream with the sum of all temporal lengths of RTP packets. + value += (now - start) - duration; + //Log.d(TAG, "sum1: "+duration/1000000+" sum2: "+(now-start)/1000000+" drift: "+((now-start)-duration)/1000000+" v: "+value/1000000); + } + if (c<5) { + // We ignore the first 20 measured values because they may not be accurate + c++; + m = value; + } else { + m = (m*q+value)/(q+1); + if (q>2; + //Log.d(TAG,"j: "+j+" buffer: "+printBuffer(rtphl, rtphl+5)+" tr: "+tr); + if (firstFragment) { + // This is the first fragment of the frame -> header is set to 0x0400 + buffer[rtphl] = 4; + firstFragment = false; + } else { + buffer[rtphl] = 0; + } + if (j>0) { + // We have found the end of the frame + stats.push(duration); + ts+= stats.average(); duration = 0; + //Log.d(TAG,"End of frame ! duration: "+stats.average()); + // The last fragment of a frame has to be marked + socket.markNextPacket(); + send(j); + nextBuffer = socket.requestBuffer(); + System.arraycopy(buffer,j+2,nextBuffer,rtphl+2,MAXPACKETSIZE-j-2); + buffer = nextBuffer; + j = MAXPACKETSIZE-j-2; + firstFragment = true; + } else { + // We have not found the beginning of another frame + // The whole packet is a fragment of a frame + send(MAXPACKETSIZE); + } + } + } catch (IOException e) { + } catch (InterruptedException e) {} + + Log.d(TAG,"H263 Packetizer stopped !"); + + } + + private int fill(int offset,int length) throws IOException { + + int sum = 0, len; + + while (sum>8); + stapa[2] = (byte) (sps.length&0xFF); + stapa[sps.length+1] = (byte) (pps.length>>8); + stapa[sps.length+2] = (byte) (pps.length&0xFF); + System.arraycopy(sps, 0, stapa, 3, sps.length); + System.arraycopy(pps, 0, stapa, 5+sps.length, pps.length); + } + } + + public void run() { + long duration = 0; + Log.d(TAG,"H264 packetizer started !"); + stats.reset(); + count = 0; + + if (is instanceof MediaCodecInputStream) { + streamType = 1; + socket.setCacheSize(0); + } else { + streamType = 0; + socket.setCacheSize(400); + } + + try { + while (!Thread.interrupted()) { + + oldtime = System.nanoTime(); + // We read a NAL units from the input stream and we send them + send(); + // We measure how long it took to receive NAL units from the phone + duration = System.nanoTime() - oldtime; + + stats.push(duration); + // Computes the average duration of a NAL unit + delay = stats.average(); + //Log.d(TAG,"duration: "+duration/1000000+" delay: "+delay/1000000); + + } + } catch (IOException e) { + } catch (InterruptedException e) {} + + Log.d(TAG,"H264 packetizer stopped !"); + + } + + /** + * Reads a NAL unit in the FIFO and sends it. + * If it is too big, we split it in FU-A units (RFC 3984). + */ + @SuppressLint("NewApi") + private void send() throws IOException, InterruptedException { + int sum = 1, len = 0, type; + + if (streamType == 0) { + // NAL units are preceeded by their length, we parse the length + fill(header,0,5); + ts += delay; + naluLength = header[3]&0xFF | (header[2]&0xFF)<<8 | (header[1]&0xFF)<<16 | (header[0]&0xFF)<<24; + if (naluLength>100000 || naluLength<0) resync(); + } else if (streamType == 1) { + // NAL units are preceeded with 0x00000001 + fill(header,0,5); + ts = ((MediaCodecInputStream)is).getLastBufferInfo().presentationTimeUs*1000L; + //ts += delay; + naluLength = is.available()+1; + if (!(header[0]==0 && header[1]==0 && header[2]==0)) { + // Turns out, the NAL units are not preceeded with 0x00000001 + Log.e(TAG, "NAL units are not preceeded by 0x00000001"); + streamType = 2; + return; + } + } else { + // Nothing preceededs the NAL units + fill(header,0,1); + header[4] = header[0]; + ts = ((MediaCodecInputStream)is).getLastBufferInfo().presentationTimeUs*1000L; + //ts += delay; + naluLength = is.available()+1; + } + + // Parses the NAL unit type + type = header[4]&0x1F; + + + // The stream already contains NAL unit type 7 or 8, we don't need + // to add them to the stream ourselves + if (type == 7 || type == 8) { + Log.v(TAG,"SPS or PPS present in the stream."); + count++; + if (count>4) { + sps = null; + pps = null; + } + } + + // We send two packets containing NALU type 7 (SPS) and 8 (PPS) + // Those should allow the H264 stream to be decoded even if no SDP was sent to the decoder. + if (type == 5 && sps != null && pps != null) { + buffer = socket.requestBuffer(); + socket.markNextPacket(); + socket.updateTimestamp(ts); + System.arraycopy(stapa, 0, buffer, rtphl, stapa.length); + super.send(rtphl+stapa.length); + } + + //Log.d(TAG,"- Nal unit length: " + naluLength + " delay: "+delay/1000000+" type: "+type); + + // Small NAL unit => Single NAL unit + if (naluLength<=MAXPACKETSIZE-rtphl-2) { + buffer = socket.requestBuffer(); + buffer[rtphl] = header[4]; + len = fill(buffer, rtphl+1, naluLength-1); + socket.updateTimestamp(ts); + socket.markNextPacket(); + super.send(naluLength+rtphl); + //Log.d(TAG,"----- Single NAL unit - len:"+len+" delay: "+delay); + } + // Large NAL unit => Split nal unit + else { + + // Set FU-A header + header[1] = (byte) (header[4] & 0x1F); // FU header type + header[1] += 0x80; // Start bit + // Set FU-A indicator + header[0] = (byte) ((header[4] & 0x60) & 0xFF); // FU indicator NRI + header[0] += 28; + + while (sum < naluLength) { + buffer = socket.requestBuffer(); + buffer[rtphl] = header[0]; + buffer[rtphl+1] = header[1]; + socket.updateTimestamp(ts); + if ((len = fill(buffer, rtphl+2, naluLength-sum > MAXPACKETSIZE-rtphl-2 ? MAXPACKETSIZE-rtphl-2 : naluLength-sum ))<0) return; sum += len; + // Last packet before next NAL + if (sum >= naluLength) { + // End bit on + buffer[rtphl+1] += 0x40; + socket.markNextPacket(); + } + super.send(len+rtphl+2); + // Switch start bit + header[1] = (byte) (header[1] & 0x7F); + //Log.d(TAG,"----- FU-A unit, sum:"+sum); + } + } + } + + private int fill(byte[] buffer, int offset,int length) throws IOException { + int sum = 0, len; + while (sum0 && naluLength<100000) { + oldtime = System.nanoTime(); + Log.e(TAG,"A NAL unit may have been found in the bit stream !"); + break; + } + if (naluLength==0) { + Log.e(TAG,"NAL unit with NULL size found..."); + } else if (header[3]==0xFF && header[2]==0xFF && header[1]==0xFF && header[0]==0xFF) { + Log.e(TAG,"NAL unit with 0xFFFFFFFF size found..."); + } + } + + } + + } + +} \ No newline at end of file diff --git a/src/main/java/net/majorkernelpanic/streaming/rtp/MediaCodecInputStream.java b/src/main/java/net/majorkernelpanic/streaming/rtp/MediaCodecInputStream.java new file mode 100644 index 00000000..b77e0f69 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/rtp/MediaCodecInputStream.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.rtp; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +import android.annotation.SuppressLint; +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaFormat; +import android.util.Log; + +/** + * An InputStream that uses data from a MediaCodec. + * The purpose of this class is to interface existing RTP packetizers of + * libstreaming with the new MediaCodec API. This class is not thread safe ! + */ +@SuppressLint("NewApi") +public class MediaCodecInputStream extends InputStream { + + public final String TAG = "MediaCodecInputStream"; + + private MediaCodec mMediaCodec = null; + private BufferInfo mBufferInfo = new BufferInfo(); + private ByteBuffer[] mBuffers = null; + private ByteBuffer mBuffer = null; + private int mIndex = -1; + private boolean mClosed = false; + + public MediaFormat mMediaFormat; + + public MediaCodecInputStream(MediaCodec mediaCodec) { + mMediaCodec = mediaCodec; + mBuffers = mMediaCodec.getOutputBuffers(); + } + + @Override + public void close() { + mClosed = true; + } + + @Override + public int read() throws IOException { + return 0; + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + int min = 0; + + try { + if (mBuffer==null) { + while (!Thread.interrupted() && !mClosed) { + mIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 500000); + if (mIndex>=0 ){ + //Log.d(TAG,"Index: "+mIndex+" Time: "+mBufferInfo.presentationTimeUs+" size: "+mBufferInfo.size); + mBuffer = mBuffers[mIndex]; + mBuffer.position(0); + break; + } else if (mIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + mBuffers = mMediaCodec.getOutputBuffers(); + } else if (mIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + mMediaFormat = mMediaCodec.getOutputFormat(); + Log.i(TAG,mMediaFormat.toString()); + } else if (mIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { + Log.v(TAG,"No buffer available..."); + //return 0; + } else { + Log.e(TAG,"Message: "+mIndex); + //return 0; + } + } + } + + if (mClosed) throw new IOException("This InputStream was closed"); + + min = length < mBufferInfo.size - mBuffer.position() ? length : mBufferInfo.size - mBuffer.position(); + mBuffer.get(buffer, offset, min); + if (mBuffer.position()>=mBufferInfo.size) { + mMediaCodec.releaseOutputBuffer(mIndex, false); + mBuffer = null; + } + + } catch (RuntimeException e) { + e.printStackTrace(); + } + + return min; + } + + public int available() { + if (mBuffer != null) + return mBufferInfo.size - mBuffer.position(); + else + return 0; + } + + public BufferInfo getLastBufferInfo() { + return mBufferInfo; + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/rtp/RtpSocket.java b/src/main/java/net/majorkernelpanic/streaming/rtp/RtpSocket.java new file mode 100644 index 00000000..9ae91e95 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/rtp/RtpSocket.java @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.rtp; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import net.majorkernelpanic.streaming.rtcp.SenderReport; +import android.os.SystemClock; +import android.util.Log; + +/** + * A basic implementation of an RTP socket. + * It implements a buffering mechanism, relying on a FIFO of buffers and a Thread. + * That way, if a packetizer tries to send many packets too quickly, the FIFO will + * grow and packets will be sent one by one smoothly. + */ +public class RtpSocket implements Runnable { + + public static final String TAG = "RtpSocket"; + + /** Use this to use UDP for the transport protocol. */ + public final static int TRANSPORT_UDP = 0x00; + + /** Use this to use TCP for the transport protocol. */ + public final static int TRANSPORT_TCP = 0x01; + + public static final int RTP_HEADER_LENGTH = 12; + public static final int MTU = 1300; + + private MulticastSocket mSocket; + private DatagramPacket[] mPackets; + private byte[][] mBuffers; + private long[] mTimestamps; + + private SenderReport mReport; + + private Semaphore mBufferRequested, mBufferCommitted; + private Thread mThread; + + private int mTransport; + private long mCacheSize; + private long mClock = 0; + private long mOldTimestamp = 0; + private int mSsrc, mSeq = 0, mPort = -1; + private int mBufferCount, mBufferIn, mBufferOut; + private int mCount = 0; + private byte mTcpHeader[]; + protected OutputStream mOutputStream = null; + + private AverageBitrate mAverageBitrate; + + /** + * This RTP socket implements a buffering mechanism relying on a FIFO of buffers and a Thread. + * @throws IOException + */ + public RtpSocket() { + + mCacheSize = 0; + mBufferCount = 300; // TODO: readjust that when the FIFO is full + mBuffers = new byte[mBufferCount][]; + mPackets = new DatagramPacket[mBufferCount]; + mReport = new SenderReport(); + mAverageBitrate = new AverageBitrate(); + mTransport = TRANSPORT_UDP; + mTcpHeader = new byte[] {'$',0,0,0}; + + resetFifo(); + + for (int i=0; i Source Identifier(0) */ + /* | || | */ + mBuffers[i][0] = (byte) Integer.parseInt("10000000",2); + + /* Payload Type */ + mBuffers[i][1] = (byte) 96; + + /* Byte 2,3 -> Sequence Number */ + /* Byte 4,5,6,7 -> Timestamp */ + /* Byte 8,9,10,11 -> Sync Source Identifier */ + + } + + try { + mSocket = new MulticastSocket(); + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + + } + + private void resetFifo() { + mCount = 0; + mBufferIn = 0; + mBufferOut = 0; + mTimestamps = new long[mBufferCount]; + mBufferRequested = new Semaphore(mBufferCount); + mBufferCommitted = new Semaphore(0); + mReport.reset(); + mAverageBitrate.reset(); + } + + /** Closes the underlying socket. */ + public void close() { + mSocket.close(); + } + + /** Sets the SSRC of the stream. */ + public void setSSRC(int ssrc) { + this.mSsrc = ssrc; + for (int i=0;i=mBufferCount) mBufferIn = 0; + mBufferCommitted.release(); + + } + + /** Sends the RTP packet over the network. */ + public void commitBuffer(int length) throws IOException { + updateSequence(); + mPackets[mBufferIn].setLength(length); + + mAverageBitrate.push(length); + + if (++mBufferIn>=mBufferCount) mBufferIn = 0; + mBufferCommitted.release(); + + if (mThread == null) { + mThread = new Thread(this); + mThread.start(); + } + + } + + /** Returns an approximation of the bitrate of the RTP stream in bits per second. */ + public long getBitrate() { + return mAverageBitrate.average(); + } + + /** Increments the sequence number. */ + private void updateSequence() { + setLong(mBuffers[mBufferIn], ++mSeq, 2, 4); + } + + /** + * Overwrites the timestamp in the packet. + * @param timestamp The new timestamp in ns. + **/ + public void updateTimestamp(long timestamp) { + mTimestamps[mBufferIn] = timestamp; + setLong(mBuffers[mBufferIn], (timestamp/100L)*(mClock/1000L)/10000L, 4, 8); + } + + /** Sets the marker in the RTP packet. */ + public void markNextPacket() { + mBuffers[mBufferIn][1] |= 0x80; + } + + /** The Thread sends the packets in the FIFO one by one at a constant rate. */ + @Override + public void run() { + Statistics stats = new Statistics(50,3000); + try { + // Caches mCacheSize milliseconds of the stream in the FIFO. + Thread.sleep(mCacheSize); + long delta = 0; + while (mBufferCommitted.tryAcquire(4,TimeUnit.SECONDS)) { + if (mOldTimestamp != 0) { + // We use our knowledge of the clock rate of the stream and the difference between two timestamps to + // compute the time lapse that the packet represents. + if ((mTimestamps[mBufferOut]-mOldTimestamp)>0) { + stats.push(mTimestamps[mBufferOut]-mOldTimestamp); + long d = stats.average()/1000000; + //Log.d(TAG,"delay: "+d+" d: "+(mTimestamps[mBufferOut]-mOldTimestamp)/1000000); + // We ensure that packets are sent at a constant and suitable rate no matter how the RtpSocket is used. + if (mCacheSize>0) Thread.sleep(d); + } else if ((mTimestamps[mBufferOut]-mOldTimestamp)<0) { + Log.e(TAG, "TS: "+mTimestamps[mBufferOut]+" OLD: "+mOldTimestamp); + } + delta += mTimestamps[mBufferOut]-mOldTimestamp; + if (delta>500000000 || delta<0) { + //Log.d(TAG,"permits: "+mBufferCommitted.availablePermits()); + delta = 0; + } + } + mReport.update(mPackets[mBufferOut].getLength(), (mTimestamps[mBufferOut]/100L)*(mClock/1000L)/10000L); + mOldTimestamp = mTimestamps[mBufferOut]; + if (mCount++>30) { + if (mTransport == TRANSPORT_UDP) { + mSocket.send(mPackets[mBufferOut]); + } else { + sendTCP(); + } + } + if (++mBufferOut>=mBufferCount) mBufferOut = 0; + mBufferRequested.release(); + } + } catch (Exception e) { + e.printStackTrace(); + } + mThread = null; + resetFifo(); + } + + private void sendTCP() { + synchronized (mOutputStream) { + int len = mPackets[mBufferOut].getLength(); + Log.d(TAG,"sent "+len); + mTcpHeader[2] = (byte) (len>>8); + mTcpHeader[3] = (byte) (len&0xFF); + try { + mOutputStream.write(mTcpHeader); + mOutputStream.write(mBuffers[mBufferOut], 0, len); + } catch (Exception e) {} + } + } + + private void setLong(byte[] buffer, long n, int begin, int end) { + for (end--; end >= begin; end--) { + buffer[end] = (byte) (n % 256); + n >>= 8; + } + } + + /** + * Computes an average bit rate. + **/ + protected static class AverageBitrate { + + private final static long RESOLUTION = 200; + + private long mOldNow, mNow, mDelta; + private long[] mElapsed, mSum; + private int mCount, mIndex, mTotal; + private int mSize; + + public AverageBitrate() { + mSize = 5000/((int)RESOLUTION); + reset(); + } + + public AverageBitrate(int delay) { + mSize = delay/((int)RESOLUTION); + reset(); + } + + public void reset() { + mSum = new long[mSize]; + mElapsed = new long[mSize]; + mNow = SystemClock.elapsedRealtime(); + mOldNow = mNow; + mCount = 0; + mDelta = 0; + mTotal = 0; + mIndex = 0; + } + + public void push(int length) { + mNow = SystemClock.elapsedRealtime(); + if (mCount>0) { + mDelta += mNow - mOldNow; + mTotal += length; + if (mDelta>RESOLUTION) { + mSum[mIndex] = mTotal; + mTotal = 0; + mElapsed[mIndex] = mDelta; + mDelta = 0; + mIndex++; + if (mIndex>=mSize) mIndex = 0; + } + } + mOldNow = mNow; + mCount++; + } + + public int average() { + long delta = 0, sum = 0; + for (int i=0;i0?8000*sum/delta:0); + } + + } + + /** Computes the proper rate at which packets are sent. */ + protected static class Statistics { + + public final static String TAG = "Statistics"; + + private int count=500, c = 0; + private float m = 0, q = 0; + private long elapsed = 0; + private long start = 0; + private long duration = 0; + private long period = 6000000000L; + private boolean initoffset = false; + + public Statistics(int count, long period) { + this.count = count; + this.period = period*1000000L; + } + + public void push(long value) { + duration += value; + elapsed += value; + if (elapsed>period) { + elapsed = 0; + long now = System.nanoTime(); + if (!initoffset || (now - start < 0)) { + start = now; + duration = 0; + initoffset = true; + } + value -= (now - start) - duration; + //Log.d(TAG, "sum1: "+duration/1000000+" sum2: "+(now-start)/1000000+" drift: "+((now-start)-duration)/1000000+" v: "+value/1000000); + } + if (c<40) { + // We ignore the first 40 measured values because they may not be accurate + c++; + m = value; + } else { + m = (m*q+value)/(q+1); + if (q0 ? l : 0; + } + + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/rtsp/RtcpDeinterleaver.java b/src/main/java/net/majorkernelpanic/streaming/rtsp/RtcpDeinterleaver.java new file mode 100644 index 00000000..8bfc3b56 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/rtsp/RtcpDeinterleaver.java @@ -0,0 +1,72 @@ +package net.majorkernelpanic.streaming.rtsp; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; + +class RtcpDeinterleaver extends InputStream implements Runnable { + + public final static String TAG = "RtcpDeinterleaver"; + + private IOException mIOException; + private InputStream mInputStream; + private PipedInputStream mPipedInputStream; + private PipedOutputStream mPipedOutputStream; + private byte[] mBuffer; + + public RtcpDeinterleaver(InputStream inputStream) { + mInputStream = inputStream; + mPipedInputStream = new PipedInputStream(4096); + try { + mPipedOutputStream = new PipedOutputStream(mPipedInputStream); + } catch (IOException e) {} + mBuffer = new byte[1024]; + new Thread(this).start(); + } + + @Override + public void run() { + try { + while (true) { + int len = mInputStream.read(mBuffer, 0, 1024); + mPipedOutputStream.write(mBuffer, 0, len); + } + } catch (IOException e) { + try { + mPipedInputStream.close(); + } catch (IOException ignore) {} + mIOException = e; + } + } + + @Override + public int read(byte[] buffer) throws IOException { + if (mIOException != null) { + throw mIOException; + } + return mPipedInputStream.read(buffer); + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + if (mIOException != null) { + throw mIOException; + } + return mPipedInputStream.read(buffer, offset, length); + } + + @Override + public int read() throws IOException { + if (mIOException != null) { + throw mIOException; + } + return mPipedInputStream.read(); + } + + @Override + public void close() throws IOException { + mInputStream.close(); + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/rtsp/RtspClient.java b/src/main/java/net/majorkernelpanic/streaming/rtsp/RtspClient.java new file mode 100644 index 00000000..d85fd4ec --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/rtsp/RtspClient.java @@ -0,0 +1,607 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.rtsp; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.net.SocketException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Locale; +import java.util.concurrent.Semaphore; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.majorkernelpanic.streaming.Session; +import net.majorkernelpanic.streaming.Stream; +import net.majorkernelpanic.streaming.rtp.RtpSocket; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.util.Log; + +/** + * RFC 2326. + * A basic and asynchronous RTSP client. + * The original purpose of this class was to implement a small RTSP client compatible with Wowza. + * It implements Digest Access Authentication according to RFC 2069. + */ +public class RtspClient { + + public final static String TAG = "RtspClient"; + + /** Message sent when the connection to the RTSP server failed. */ + public final static int ERROR_CONNECTION_FAILED = 0x01; + + /** Message sent when the credentials are wrong. */ + public final static int ERROR_WRONG_CREDENTIALS = 0x03; + + /** Use this to use UDP for the transport protocol. */ + public final static int TRANSPORT_UDP = RtpSocket.TRANSPORT_UDP; + + /** Use this to use TCP for the transport protocol. */ + public final static int TRANSPORT_TCP = RtpSocket.TRANSPORT_TCP; + + /** + * Message sent when the connection with the RTSP server has been lost for + * some reason (for example, the user is going under a bridge). + * When the connection with the server is lost, the client will automatically try to + * reconnect as long as {@link #stopStream()} is not called. + **/ + public final static int ERROR_CONNECTION_LOST = 0x04; + + /** + * Message sent when the connection with the RTSP server has been reestablished. + * When the connection with the server is lost, the client will automatically try to + * reconnect as long as {@link #stopStream()} is not called. + */ + public final static int MESSAGE_CONNECTION_RECOVERED = 0x05; + + private final static int STATE_STARTED = 0x00; + private final static int STATE_STARTING = 0x01; + private final static int STATE_STOPPING = 0x02; + private final static int STATE_STOPPED = 0x03; + private int mState = 0; + + private class Parameters { + public String host; + public String username; + public String password; + public String path; + public Session session; + public int port; + public int transport; + + public Parameters clone() { + Parameters params = new Parameters(); + params.host = host; + params.username = username; + params.password = password; + params.path = path; + params.session = session; + params.port = port; + params.transport = transport; + return params; + } + } + + + private Parameters mTmpParameters; + private Parameters mParameters; + + private int mCSeq; + private Socket mSocket; + private String mSessionID; + private String mAuthorization; + private BufferedReader mBufferedReader; + private OutputStream mOutputStream; + private Callback mCallback; + private Handler mMainHandler; + private Handler mHandler; + + /** + * The callback interface you need to implement to know what's going on with the + * RTSP server (for example your Wowza Media Server). + */ + public interface Callback { + public void onRtspUpdate(int message, Exception exception); + } + + public RtspClient() { + mCSeq = 0; + mTmpParameters = new Parameters(); + mTmpParameters.port = 1935; + mTmpParameters.path = "/"; + mTmpParameters.transport = TRANSPORT_UDP; + mAuthorization = null; + mCallback = null; + mMainHandler = new Handler(Looper.getMainLooper()); + mState = STATE_STOPPED; + + final Semaphore signal = new Semaphore(0); + new HandlerThread("net.majorkernelpanic.streaming.RtspClient"){ + @Override + protected void onLooperPrepared() { + mHandler = new Handler(); + signal.release(); + } + }.start(); + signal.acquireUninterruptibly(); + + } + + /** + * Sets the callback interface that will be called on status updates of the connection + * with the RTSP server. + * @param cb The implementation of the {@link Callback} interface + */ + public void setCallback(Callback cb) { + mCallback = cb; + } + + /** + * The {@link Session} that will be used to stream to the server. + * If not called before {@link #startStream()}, a it will be created. + */ + public void setSession(Session session) { + mTmpParameters.session = session; + } + + public Session getSession() { + return mTmpParameters.session; + } + + /** + * Sets the destination address of the RTSP server. + * @param host The destination address + * @param port The destination port + */ + public void setServerAddress(String host, int port) { + mTmpParameters.port = port; + mTmpParameters.host = host; + } + + /** + * If authentication is enabled on the server, you need to call this with a valid username/password pair. + * Only implements Digest Access Authentication according to RFC 2069. + * @param username The username + * @param password The password + */ + public void setCredentials(String username, String password) { + mTmpParameters.username = username; + mTmpParameters.password = password; + } + + /** + * The path to which the stream will be sent to. + * @param path The path + */ + public void setStreamPath(String path) { + mTmpParameters.path = path; + } + + /** + * Call this with {@link #TRANSPORT_TCP} or {@value #TRANSPORT_UDP} to choose the + * transport protocol that will be used to send RTP/RTCP packets. + * Not ready yet ! + */ + public void setTransportMode(int mode) { + mTmpParameters.transport = mode; + } + + public boolean isStreaming() { + return mState==STATE_STARTED|mState==STATE_STARTING; + } + + /** + * Connects to the RTSP server to publish the stream, and the effectively starts streaming. + * You need to call {@link #setServerAddress(String, int)} and optionnally {@link #setSession(Session)} + * and {@link #setCredentials(String, String)} before calling this. + * Should be called of the main thread ! + */ + public void startStream() { + if (mTmpParameters.host == null) throw new IllegalStateException("setServerAddress(String,int) has not been called !"); + if (mTmpParameters.session == null) throw new IllegalStateException("setSession() has not been called !"); + mHandler.post(new Runnable () { + @Override + public void run() { + if (mState != STATE_STOPPED) return; + mState = STATE_STARTING; + + Log.d(TAG,"Connecting to RTSP server..."); + + // If the user calls some methods to configure the client, it won't modify its behavior until the stream is restarted + mParameters = mTmpParameters.clone(); + mParameters.session.setDestination(mTmpParameters.host); + + try { + mParameters.session.syncConfigure(); + } catch (Exception e) { + mParameters.session = null; + mState = STATE_STOPPED; + return; + } + + try { + tryConnection(); + } catch (Exception e) { + postError(ERROR_CONNECTION_FAILED, e); + abort(); + return; + } + + try { + mParameters.session.syncStart(); + mState = STATE_STARTED; + if (mParameters.transport == TRANSPORT_UDP) { + mHandler.post(mConnectionMonitor); + } + } catch (Exception e) { + abort(); + } + + } + }); + + } + + /** + * Stops the stream, and informs the RTSP server. + */ + public void stopStream() { + mHandler.post(new Runnable () { + @Override + public void run() { + if (mParameters != null && mParameters.session != null) { + mParameters.session.stop(); + } + if (mState != STATE_STOPPED) { + mState = STATE_STOPPING; + abort(); + } + } + }); + } + + public void release() { + stopStream(); + mHandler.getLooper().quit(); + } + + private void abort() { + try { + sendRequestTeardown(); + } catch (Exception ignore) {} + try { + mSocket.close(); + } catch (Exception ignore) {} + mHandler.removeCallbacks(mConnectionMonitor); + mHandler.removeCallbacks(mRetryConnection); + mState = STATE_STOPPED; + } + + private void tryConnection() throws IOException { + mCSeq = 0; + mSocket = new Socket(mParameters.host, mParameters.port); + mBufferedReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream())); + mOutputStream = new BufferedOutputStream(mSocket.getOutputStream()); + sendRequestAnnounce(); + sendRequestSetup(); + sendRequestRecord(); + } + + /** + * Forges and sends the ANNOUNCE request + */ + private void sendRequestAnnounce() throws IllegalStateException, SocketException, IOException { + + String body = mParameters.session.getSessionDescription(); + String request = "ANNOUNCE rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + + "CSeq: " + (++mCSeq) + "\r\n" + + "Content-Length: " + body.length() + "\r\n" + + "Content-Type: application/sdp \r\n\r\n" + + body; + Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); + + mOutputStream.write(request.getBytes("UTF-8")); + mOutputStream.flush(); + Response response = Response.parseResponse(mBufferedReader); + + if (response.headers.containsKey("server")) { + Log.v(TAG,"RTSP server name:" + response.headers.get("server")); + } else { + Log.v(TAG,"RTSP server name unknown"); + } + + try { + Matcher m = Response.rexegSession.matcher(response.headers.get("session")); + m.find(); + mSessionID = m.group(1); + } catch (Exception e) { + throw new IOException("Invalid response from server. Session id: "+mSessionID); + } + + if (response.status == 401) { + String nonce, realm; + Matcher m; + + if (mParameters.username == null || mParameters.password == null) throw new IllegalStateException("Authentication is enabled and setCredentials(String,String) was not called !"); + + try { + m = Response.rexegAuthenticate.matcher(response.headers.get("www-authenticate")); m.find(); + nonce = m.group(2); + realm = m.group(1); + } catch (Exception e) { + throw new IOException("Invalid response from server"); + } + + String uri = "rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path; + String hash1 = computeMd5Hash(mParameters.username+":"+m.group(1)+":"+mParameters.password); + String hash2 = computeMd5Hash("ANNOUNCE"+":"+uri); + String hash3 = computeMd5Hash(hash1+":"+m.group(2)+":"+hash2); + + mAuthorization = "Digest username=\""+mParameters.username+"\",realm=\""+realm+"\",nonce=\""+nonce+"\",uri=\""+uri+"\",response=\""+hash3+"\""; + + request = "ANNOUNCE rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + + "CSeq: " + (++mCSeq) + "\r\n" + + "Content-Length: " + body.length() + "\r\n" + + "Authorization: " + mAuthorization + "\r\n" + + "Session: " + mSessionID + "\r\n" + + "Content-Type: application/sdp \r\n\r\n" + + body; + + Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); + + mOutputStream.write(request.getBytes("UTF-8")); + mOutputStream.flush(); + response = Response.parseResponse(mBufferedReader); + + if (response.status == 401) throw new RuntimeException("Bad credentials !"); + + } else if (response.status == 403) { + throw new RuntimeException("Access forbidden !"); + } + + } + + /** + * Forges and sends the SETUP request + */ + private void sendRequestSetup() throws IllegalStateException, SocketException, IOException { + for (int i=0;i<2;i++) { + Stream stream = mParameters.session.getTrack(i); + if (stream != null) { + String params = mParameters.transport==TRANSPORT_TCP ? + ("TCP;interleaved="+2*i+"-"+(2*i+1)) : ("UDP;unicast;client_port="+(5000+2*i)+"-"+(5000+2*i+1)+";mode=receive"); + String request = "SETUP rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+"/trackID="+i+" RTSP/1.0\r\n" + + "Transport: RTP/AVP/"+params+"\r\n" + + addHeaders(); + + Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); + + mOutputStream.write(request.getBytes("UTF-8")); + mOutputStream.flush(); + Response response = Response.parseResponse(mBufferedReader); + Matcher m; + if (mParameters.transport == TRANSPORT_UDP) { + try { + m = Response.rexegTransport.matcher(response.headers.get("transport")); m.find(); + stream.setDestinationPorts(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4))); + Log.d(TAG, "Setting destination ports: "+Integer.parseInt(m.group(3))+", "+Integer.parseInt(m.group(4))); + } catch (Exception e) { + e.printStackTrace(); + int[] ports = stream.getDestinationPorts(); + Log.d(TAG,"Server did not specify ports, using default ports: "+ports[0]+"-"+ports[1]); + } + } else { + stream.setOutputStream(mOutputStream, (byte)(2*i)); + } + } + } + } + + /** + * Forges and sends the RECORD request + */ + private void sendRequestRecord() throws IllegalStateException, SocketException, IOException { + String request = "RECORD rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + + "Range: npt=0.000-\r\n" + + addHeaders(); + Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); + mOutputStream.write(request.getBytes("UTF-8")); + mOutputStream.flush(); + Response.parseResponse(mBufferedReader); + } + + /** + * Forges and sends the TEARDOWN request + */ + private void sendRequestTeardown() throws IOException { + String request = "TEARDOWN rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + addHeaders(); + Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); + mOutputStream.write(request.getBytes("UTF-8")); + mOutputStream.flush(); + } + + /** + * Forges and sends the OPTIONS request + */ + private void sendRequestOption() throws IOException { + String request = "OPTIONS rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + addHeaders(); + Log.i(TAG,request.substring(0, request.indexOf("\r\n"))); + mOutputStream.write(request.getBytes("UTF-8")); + mOutputStream.flush(); + Response.parseResponse(mBufferedReader); + } + + private String addHeaders() { + return "CSeq: " + (++mCSeq) + "\r\n" + + "Content-Length: 0\r\n" + + "Session: " + mSessionID + "\r\n" + + // For some reason you may have to remove last "\r\n" in the next line to make the RTSP client work with your wowza server :/ + (mAuthorization != null ? "Authorization: " + mAuthorization + "\r\n":"") + "\r\n"; + } + + /** + * If the connection with the RTSP server is lost, we try to reconnect to it as + * long as {@link #stopStream()} is not called. + */ + private Runnable mConnectionMonitor = new Runnable() { + @Override + public void run() { + if (mState == STATE_STARTED) { + try { + // We poll the RTSP server with OPTION requests + sendRequestOption(); + mHandler.postDelayed(mConnectionMonitor, 6000); + } catch (IOException e) { + // Happens if the OPTION request fails + postMessage(ERROR_CONNECTION_LOST); + Log.e(TAG, "Connection lost with the server..."); + mParameters.session.stop(); + mHandler.post(mRetryConnection); + } + } + } + }; + + /** Here, we try to reconnect to the RTSP. */ + private Runnable mRetryConnection = new Runnable() { + @Override + public void run() { + if (mState == STATE_STARTED) { + try { + Log.e(TAG, "Trying to reconnect..."); + tryConnection(); + try { + mParameters.session.start(); + mHandler.post(mConnectionMonitor); + postMessage(MESSAGE_CONNECTION_RECOVERED); + } catch (Exception e) { + abort(); + } + } catch (IOException e) { + mHandler.postDelayed(mRetryConnection,1000); + } + } + } + }; + + final protected static char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; + + private static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + int v; + for ( int j = 0; j < bytes.length; j++ ) { + v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + /** Needed for the Digest Access Authentication. */ + private String computeMd5Hash(String buffer) { + MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + return bytesToHex(md.digest(buffer.getBytes("UTF-8"))); + } catch (NoSuchAlgorithmException ignore) { + } catch (UnsupportedEncodingException e) {} + return ""; + } + + private void postMessage(final int message) { + mMainHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onRtspUpdate(message, null); + } + } + }); + } + + private void postError(final int message, final Exception e) { + mMainHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onRtspUpdate(message, e); + } + } + }); + } + + static class Response { + + // Parses method & uri + public static final Pattern regexStatus = Pattern.compile("RTSP/\\d.\\d (\\d+) (\\w+)",Pattern.CASE_INSENSITIVE); + // Parses a request header + public static final Pattern rexegHeader = Pattern.compile("(\\S+):(.+)",Pattern.CASE_INSENSITIVE); + // Parses a WWW-Authenticate header + public static final Pattern rexegAuthenticate = Pattern.compile("realm=\"(.+)\",\\s+nonce=\"(\\w+)\"",Pattern.CASE_INSENSITIVE); + // Parses a Session header + public static final Pattern rexegSession = Pattern.compile("(\\d+)",Pattern.CASE_INSENSITIVE); + // Parses a Transport header + public static final Pattern rexegTransport = Pattern.compile("client_port=(\\d+)-(\\d+).+server_port=(\\d+)-(\\d+)",Pattern.CASE_INSENSITIVE); + + + public int status; + public HashMap headers = new HashMap(); + + /** Parse the method, URI & headers of a RTSP request */ + public static Response parseResponse(BufferedReader input) throws IOException, IllegalStateException, SocketException { + Response response = new Response(); + String line; + Matcher matcher; + // Parsing request method & URI + if ((line = input.readLine())==null) throw new SocketException("Connection lost"); + matcher = regexStatus.matcher(line); + matcher.find(); + response.status = Integer.parseInt(matcher.group(1)); + + // Parsing headers of the request + while ( (line = input.readLine()) != null) { + //Log.e(TAG,"l: "+line.length()+", c: "+line); + if (line.length()>3) { + matcher = rexegHeader.matcher(line); + matcher.find(); + response.headers.put(matcher.group(1).toLowerCase(Locale.US),matcher.group(2)); + } else { + break; + } + } + if (line==null) throw new SocketException("Connection lost"); + + Log.d(TAG, "Response from server: "+response.status); + + return response; + } + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/rtsp/RtspServer.java b/src/main/java/net/majorkernelpanic/streaming/rtsp/RtspServer.java new file mode 100644 index 00000000..4696f973 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/rtsp/RtspServer.java @@ -0,0 +1,655 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.rtsp; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.BindException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Locale; +import java.util.WeakHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.majorkernelpanic.streaming.Session; +import net.majorkernelpanic.streaming.SessionBuilder; +import android.app.Service; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Binder; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.util.Log; + +/** + * Implementation of a subset of the RTSP protocol (RFC 2326). + * + * It allows remote control of an android device cameras & microphone. + * For each connected client, a Session is instantiated. + * The Session will start or stop streams according to what the client wants. + * + */ +public class RtspServer extends Service { + + public final static String TAG = "RtspServer"; + + /** The server name that will appear in responses. */ + public static String SERVER_NAME = "MajorKernelPanic RTSP Server"; + + /** Port used by default. */ + public static final int DEFAULT_RTSP_PORT = 8086; + + /** Port already in use. */ + public final static int ERROR_BIND_FAILED = 0x00; + + /** A stream could not be started. */ + public final static int ERROR_START_FAILED = 0x01; + + /** Streaming started. */ + public final static int MESSAGE_STREAMING_STARTED = 0X00; + + /** Streaming stopped. */ + public final static int MESSAGE_STREAMING_STOPPED = 0X01; + + /** Key used in the SharedPreferences to store whether the RTSP server is enabled or not. */ + public final static String KEY_ENABLED = "rtsp_enabled"; + + /** Key used in the SharedPreferences for the port used by the RTSP server. */ + public final static String KEY_PORT = "rtsp_port"; + + protected SessionBuilder mSessionBuilder; + protected SharedPreferences mSharedPreferences; + protected boolean mEnabled = true; + protected int mPort = DEFAULT_RTSP_PORT; + protected WeakHashMap mSessions = new WeakHashMap(2); + + private RequestListener mListenerThread; + private final IBinder mBinder = new LocalBinder(); + private boolean mRestart = false; + private final LinkedList mListeners = new LinkedList(); + + + public RtspServer() { + } + + /** Be careful: those callbacks won't necessarily be called from the ui thread ! */ + public interface CallbackListener { + + /** Called when an error occurs. */ + void onError(RtspServer server, Exception e, int error); + + /** Called when streaming starts/stops. */ + void onMessage(RtspServer server, int message); + + } + + /** + * See {@link RtspServer.CallbackListener} to check out what events will be fired once you set up a listener. + * @param listener The listener + */ + public void addCallbackListener(CallbackListener listener) { + synchronized (mListeners) { + if (mListeners.size() > 0) { + for (CallbackListener cl : mListeners) { + if (cl == listener) return; + } + } + mListeners.add(listener); + } + } + + /** + * Removes the listener. + * @param listener The listener + */ + public void removeCallbackListener(CallbackListener listener) { + synchronized (mListeners) { + mListeners.remove(listener); + } + } + + /** Returns the port used by the RTSP server. */ + public int getPort() { + return mPort; + } + + /** + * Sets the port for the RTSP server to use. + * @param port The port + */ + public void setPort(int port) { + Editor editor = mSharedPreferences.edit(); + editor.putString(KEY_PORT, String.valueOf(port)); + editor.commit(); + } + + /** + * Starts (or restart if needed, if for example the configuration + * of the server has been modified) the RTSP server. + */ + public void start() { + if (!mEnabled || mRestart) stop(); + if (mEnabled && mListenerThread == null) { + try { + mListenerThread = new RequestListener(); + } catch (Exception e) { + mListenerThread = null; + } + } + mRestart = false; + } + + /** + * Stops the RTSP server but not the Android Service. + * To stop the Android Service you need to call {@link android.content.Context#stopService(Intent)}; + */ + public void stop() { + if (mListenerThread != null) { + try { + mListenerThread.kill(); + for ( Session session : mSessions.keySet() ) { + if ( session != null ) { + if (session.isStreaming()) session.stop(); + } + } + } catch (Exception e) { + } finally { + mListenerThread = null; + } + } + } + + /** Returns whether or not the RTSP server is streaming to some client(s). */ + public boolean isStreaming() { + for ( Session session : mSessions.keySet() ) { + if ( session != null ) { + if (session.isStreaming()) return true; + } + } + return false; + } + + public boolean isEnabled() { + return mEnabled; + } + + /** Returns the bandwidth consumed by the RTSP server in bits per second. */ + public long getBitrate() { + long bitrate = 0; + for ( Session session : mSessions.keySet() ) { + if ( session != null ) { + if (session.isStreaming()) bitrate += session.getBitrate(); + } + } + return bitrate; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return START_STICKY; + } + + @Override + public void onCreate() { + + // Let's restore the state of the service + mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + mPort = Integer.parseInt(mSharedPreferences.getString(KEY_PORT, String.valueOf(mPort))); + mEnabled = mSharedPreferences.getBoolean(KEY_ENABLED, mEnabled); + + // If the configuration is modified, the server will adjust + mSharedPreferences.registerOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener); + + start(); + } + + @Override + public void onDestroy() { + stop(); + mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener); + } + + private OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + + if (key.equals(KEY_PORT)) { + int port = Integer.parseInt(sharedPreferences.getString(KEY_PORT, String.valueOf(mPort))); + if (port != mPort) { + mPort = port; + mRestart = true; + start(); + } + } + else if (key.equals(KEY_ENABLED)) { + mEnabled = sharedPreferences.getBoolean(KEY_ENABLED, mEnabled); + start(); + } + } + }; + + /** The Binder you obtain when a connection with the Service is established. */ + public class LocalBinder extends Binder { + public RtspServer getService() { + return RtspServer.this; + } + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + protected void postMessage(int id) { + synchronized (mListeners) { + if (mListeners.size() > 0) { + for (CallbackListener cl : mListeners) { + cl.onMessage(this, id); + } + } + } + } + + protected void postError(Exception exception, int id) { + synchronized (mListeners) { + if (mListeners.size() > 0) { + for (CallbackListener cl : mListeners) { + cl.onError(this, exception, id); + } + } + } + } + + /** + * By default the RTSP uses {@link UriParser} to parse the URI requested by the client + * but you can change that behavior by override this method. + * @param uri The uri that the client has requested + * @param client The socket associated to the client + * @return A proper session + */ + protected Session handleRequest(String uri, Socket client) throws IllegalStateException, IOException { + Session session = UriParser.parse(uri); + session.setOrigin(client.getLocalAddress().getHostAddress()); + if (session.getDestination()==null) { + session.setDestination(client.getInetAddress().getHostAddress()); + } + return session; + } + + class RequestListener extends Thread implements Runnable { + + private final ServerSocket mServer; + + public RequestListener() throws IOException { + try { + mServer = new ServerSocket(mPort); + start(); + } catch (BindException e) { + Log.e(TAG,"Port already in use !"); + postError(e, ERROR_BIND_FAILED); + throw e; + } + } + + public void run() { + Log.i(TAG,"RTSP server listening on port "+mServer.getLocalPort()); + while (!Thread.interrupted()) { + try { + new WorkerThread(mServer.accept()).start(); + } catch (SocketException e) { + break; + } catch (IOException e) { + Log.e(TAG,e.getMessage()); + continue; + } + } + Log.i(TAG,"RTSP server stopped !"); + } + + public void kill() { + try { + mServer.close(); + } catch (IOException e) {} + try { + this.join(); + } catch (InterruptedException ignore) {} + } + + } + + // One thread per client + class WorkerThread extends Thread implements Runnable { + + private final Socket mClient; + private final OutputStream mOutput; + private final BufferedReader mInput; + + // Each client has an associated session + private Session mSession; + + public WorkerThread(final Socket client) throws IOException { + mInput = new BufferedReader(new InputStreamReader(client.getInputStream())); + mOutput = client.getOutputStream(); + mClient = client; + mSession = new Session(); + } + + public void run() { + Request request; + Response response; + + Log.i(TAG, "Connection from "+mClient.getInetAddress().getHostAddress()); + + while (!Thread.interrupted()) { + + request = null; + response = null; + + // Parse the request + try { + request = Request.parseRequest(mInput); + } catch (SocketException e) { + // Client has left + break; + } catch (Exception e) { + // We don't understand the request :/ + response = new Response(); + response.status = Response.STATUS_BAD_REQUEST; + } + + // Do something accordingly like starting the streams, sending a session description + if (request != null) { + try { + response = processRequest(request); + } + catch (Exception e) { + // This alerts the main thread that something has gone wrong in this thread + postError(e, ERROR_START_FAILED); + Log.e(TAG,e.getMessage()!=null?e.getMessage():"An error occurred"); + e.printStackTrace(); + response = new Response(request); + } + } + + // We always send a response + // The client will receive an "INTERNAL SERVER ERROR" if an exception has been thrown at some point + try { + response.send(mOutput); + } catch (IOException e) { + Log.e(TAG,"Response was not sent properly"); + break; + } + + } + + // Streaming stops when client disconnects + boolean streaming = isStreaming(); + mSession.syncStop(); + if (streaming && !isStreaming()) { + postMessage(MESSAGE_STREAMING_STOPPED); + } + mSession.release(); + + try { + mClient.close(); + } catch (IOException ignore) {} + + Log.i(TAG, "Client disconnected"); + + } + + public Response processRequest(Request request) throws IllegalStateException, IOException { + Response response = new Response(request); + + /* ********************************************************************************** */ + /* ********************************* Method DESCRIBE ******************************** */ + /* ********************************************************************************** */ + if (request.method.equalsIgnoreCase("DESCRIBE")) { + + // Parse the requested URI and configure the session + mSession = handleRequest(request.uri, mClient); + mSessions.put(mSession, null); + mSession.syncConfigure(); + + String requestContent = mSession.getSessionDescription(); + String requestAttributes = + "Content-Base: "+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/\r\n" + + "Content-Type: application/sdp\r\n"; + + response.attributes = requestAttributes; + response.content = requestContent; + + // If no exception has been thrown, we reply with OK + response.status = Response.STATUS_OK; + + } + + /* ********************************************************************************** */ + /* ********************************* Method OPTIONS ********************************* */ + /* ********************************************************************************** */ + else if (request.method.equalsIgnoreCase("OPTIONS")) { + response.status = Response.STATUS_OK; + response.attributes = "Public: DESCRIBE,SETUP,TEARDOWN,PLAY,PAUSE\r\n"; + response.status = Response.STATUS_OK; + } + + /* ********************************************************************************** */ + /* ********************************** Method SETUP ********************************** */ + /* ********************************************************************************** */ + else if (request.method.equalsIgnoreCase("SETUP")) { + Pattern p; Matcher m; + int p2, p1, ssrc, trackId, src[]; + String destination; + + p = Pattern.compile("trackID=(\\w+)",Pattern.CASE_INSENSITIVE); + m = p.matcher(request.uri); + + if (!m.find()) { + response.status = Response.STATUS_BAD_REQUEST; + return response; + } + + trackId = Integer.parseInt(m.group(1)); + + if (!mSession.trackExists(trackId)) { + response.status = Response.STATUS_NOT_FOUND; + return response; + } + + p = Pattern.compile("client_port=(\\d+)-(\\d+)",Pattern.CASE_INSENSITIVE); + m = p.matcher(request.headers.get("transport")); + + if (!m.find()) { + int[] ports = mSession.getTrack(trackId).getDestinationPorts(); + p1 = ports[0]; + p2 = ports[1]; + } + else { + p1 = Integer.parseInt(m.group(1)); + p2 = Integer.parseInt(m.group(2)); + } + + ssrc = mSession.getTrack(trackId).getSSRC(); + src = mSession.getTrack(trackId).getLocalPorts(); + destination = mSession.getDestination(); + + mSession.getTrack(trackId).setDestinationPorts(p1, p2); + + boolean streaming = isStreaming(); + mSession.syncStart(trackId); + if (!streaming && isStreaming()) { + postMessage(MESSAGE_STREAMING_STARTED); + } + + response.attributes = "Transport: RTP/AVP/UDP;"+(InetAddress.getByName(destination).isMulticastAddress()?"multicast":"unicast")+ + ";destination="+mSession.getDestination()+ + ";client_port="+p1+"-"+p2+ + ";server_port="+src[0]+"-"+src[1]+ + ";ssrc="+Integer.toHexString(ssrc)+ + ";mode=play\r\n" + + "Session: "+ "1185d20035702ca" + "\r\n" + + "Cache-Control: no-cache\r\n"; + response.status = Response.STATUS_OK; + + // If no exception has been thrown, we reply with OK + response.status = Response.STATUS_OK; + + } + + /* ********************************************************************************** */ + /* ********************************** Method PLAY *********************************** */ + /* ********************************************************************************** */ + else if (request.method.equalsIgnoreCase("PLAY")) { + String requestAttributes = "RTP-Info: "; + if (mSession.trackExists(0)) requestAttributes += "url=rtsp://"+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/trackID="+0+";seq=0,"; + if (mSession.trackExists(1)) requestAttributes += "url=rtsp://"+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/trackID="+1+";seq=0,"; + requestAttributes = requestAttributes.substring(0, requestAttributes.length()-1) + "\r\nSession: 1185d20035702ca\r\n"; + + response.attributes = requestAttributes; + + // If no exception has been thrown, we reply with OK + response.status = Response.STATUS_OK; + + } + + /* ********************************************************************************** */ + /* ********************************** Method PAUSE ********************************** */ + /* ********************************************************************************** */ + else if (request.method.equalsIgnoreCase("PAUSE")) { + response.status = Response.STATUS_OK; + } + + /* ********************************************************************************** */ + /* ********************************* Method TEARDOWN ******************************** */ + /* ********************************************************************************** */ + else if (request.method.equalsIgnoreCase("TEARDOWN")) { + response.status = Response.STATUS_OK; + } + + /* ********************************************************************************** */ + /* ********************************* Unknown method ? ******************************* */ + /* ********************************************************************************** */ + else { + Log.e(TAG,"Command unknown: "+request); + response.status = Response.STATUS_BAD_REQUEST; + } + + return response; + + } + + } + + static class Request { + + // Parse method & uri + public static final Pattern regexMethod = Pattern.compile("(\\w+) (\\S+) RTSP",Pattern.CASE_INSENSITIVE); + // Parse a request header + public static final Pattern rexegHeader = Pattern.compile("(\\S+):(.+)",Pattern.CASE_INSENSITIVE); + + public String method; + public String uri; + public HashMap headers = new HashMap(); + + /** Parse the method, uri & headers of a RTSP request */ + public static Request parseRequest(BufferedReader input) throws IOException, IllegalStateException, SocketException { + Request request = new Request(); + String line; + Matcher matcher; + + // Parsing request method & uri + if ((line = input.readLine())==null) throw new SocketException("Client disconnected"); + matcher = regexMethod.matcher(line); + matcher.find(); + request.method = matcher.group(1); + request.uri = matcher.group(2); + + // Parsing headers of the request + while ( (line = input.readLine()) != null && line.length()>3 ) { + matcher = rexegHeader.matcher(line); + matcher.find(); + request.headers.put(matcher.group(1).toLowerCase(Locale.US),matcher.group(2)); + } + if (line==null) throw new SocketException("Client disconnected"); + + // It's not an error, it's just easier to follow what's happening in logcat with the request in red + Log.e(TAG,request.method+" "+request.uri); + + return request; + } + } + + static class Response { + + // Status code definitions + public static final String STATUS_OK = "200 OK"; + public static final String STATUS_BAD_REQUEST = "400 Bad Request"; + public static final String STATUS_NOT_FOUND = "404 Not Found"; + public static final String STATUS_INTERNAL_SERVER_ERROR = "500 Internal Server Error"; + + public String status = STATUS_INTERNAL_SERVER_ERROR; + public String content = ""; + public String attributes = ""; + + private final Request mRequest; + + public Response(Request request) { + this.mRequest = request; + } + + public Response() { + // Be carefull if you modify the send() method because request might be null ! + mRequest = null; + } + + public void send(OutputStream output) throws IOException { + int seqid = -1; + + try { + seqid = Integer.parseInt(mRequest.headers.get("cseq").replace(" ","")); + } catch (Exception e) { + Log.e(TAG,"Error parsing CSeq: "+(e.getMessage()!=null?e.getMessage():"")); + } + + String response = "RTSP/1.0 "+status+"\r\n" + + "Server: "+SERVER_NAME+"\r\n" + + (seqid>=0?("Cseq: " + seqid + "\r\n"):"") + + "Content-Length: " + content.length() + "\r\n" + + attributes + + "\r\n" + + content; + + Log.d(TAG,response.replace("\r", "")); + + output.write(response.getBytes()); + } + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/rtsp/UriParser.java b/src/main/java/net/majorkernelpanic/streaming/rtsp/UriParser.java new file mode 100644 index 00000000..23535286 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/rtsp/UriParser.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.rtsp; + +import static net.majorkernelpanic.streaming.SessionBuilder.AUDIO_AAC; +import static net.majorkernelpanic.streaming.SessionBuilder.AUDIO_AMRNB; +import static net.majorkernelpanic.streaming.SessionBuilder.AUDIO_NONE; +import static net.majorkernelpanic.streaming.SessionBuilder.VIDEO_H263; +import static net.majorkernelpanic.streaming.SessionBuilder.VIDEO_H264; +import static net.majorkernelpanic.streaming.SessionBuilder.VIDEO_NONE; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.util.Iterator; +import java.util.List; + +import net.majorkernelpanic.streaming.MediaStream; +import net.majorkernelpanic.streaming.Session; +import net.majorkernelpanic.streaming.SessionBuilder; +import net.majorkernelpanic.streaming.audio.AudioQuality; +import net.majorkernelpanic.streaming.video.VideoQuality; + +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; + +import android.hardware.Camera.CameraInfo; + +/** + * This class parses URIs received by the RTSP server and configures a Session accordingly. + */ +public class UriParser { + + public final static String TAG = "UriParser"; + + /** + * Configures a Session according to the given URI. + * Here are some examples of URIs that can be used to configure a Session: + *
  • rtsp://xxx.xxx.xxx.xxx:8086?h264&flash=on
  • + *
  • rtsp://xxx.xxx.xxx.xxx:8086?h263&camera=front&flash=on
  • + *
  • rtsp://xxx.xxx.xxx.xxx:8086?h264=200-20-320-240
  • + *
  • rtsp://xxx.xxx.xxx.xxx:8086?aac
+ * @param uri The URI + * @throws IllegalStateException + * @throws IOException + * @return A Session configured according to the URI + */ + public static Session parse(String uri) throws IllegalStateException, IOException { + SessionBuilder builder = SessionBuilder.getInstance().clone(); + byte audioApi = 0, videoApi = 0; + + List params = URLEncodedUtils.parse(URI.create(uri),"UTF-8"); + if (params.size()>0) { + + builder.setAudioEncoder(AUDIO_NONE).setVideoEncoder(VIDEO_NONE); + + // Those parameters must be parsed first or else they won't necessarily be taken into account + for (Iterator it = params.iterator();it.hasNext();) { + NameValuePair param = it.next(); + + // FLASH ON/OFF + if (param.getName().equalsIgnoreCase("flash")) { + if (param.getValue().equalsIgnoreCase("on")) + builder.setFlashEnabled(true); + else + builder.setFlashEnabled(false); + } + + // CAMERA -> the client can choose between the front facing camera and the back facing camera + else if (param.getName().equalsIgnoreCase("camera")) { + if (param.getValue().equalsIgnoreCase("back")) + builder.setCamera(CameraInfo.CAMERA_FACING_BACK); + else if (param.getValue().equalsIgnoreCase("front")) + builder.setCamera(CameraInfo.CAMERA_FACING_FRONT); + } + + // MULTICAST -> the stream will be sent to a multicast group + // The default mutlicast address is 228.5.6.7, but the client can specify another + else if (param.getName().equalsIgnoreCase("multicast")) { + if (param.getValue()!=null) { + try { + InetAddress addr = InetAddress.getByName(param.getValue()); + if (!addr.isMulticastAddress()) { + throw new IllegalStateException("Invalid multicast address !"); + } + builder.setDestination(param.getValue()); + } catch (UnknownHostException e) { + throw new IllegalStateException("Invalid multicast address !"); + } + } + else { + // Default multicast address + builder.setDestination("228.5.6.7"); + } + } + + // UNICAST -> the client can use this to specify where he wants the stream to be sent + else if (param.getName().equalsIgnoreCase("unicast")) { + if (param.getValue()!=null) { + builder.setDestination(param.getValue()); + } + } + + // VIDEOAPI -> can be used to specify what api will be used to encode video (the MediaRecorder API or the MediaCodec API) + else if (param.getName().equalsIgnoreCase("videoapi")) { + if (param.getValue()!=null) { + if (param.getValue().equalsIgnoreCase("mr")) { + videoApi = MediaStream.MODE_MEDIARECORDER_API; + } else if (param.getValue().equalsIgnoreCase("mc")) { + videoApi = MediaStream.MODE_MEDIACODEC_API; + } + } + } + + // AUDIOAPI -> can be used to specify what api will be used to encode audio (the MediaRecorder API or the MediaCodec API) + else if (param.getName().equalsIgnoreCase("audioapi")) { + if (param.getValue()!=null) { + if (param.getValue().equalsIgnoreCase("mr")) { + audioApi = MediaStream.MODE_MEDIARECORDER_API; + } else if (param.getValue().equalsIgnoreCase("mc")) { + audioApi = MediaStream.MODE_MEDIACODEC_API; + } + } + } + + // TTL -> the client can modify the time to live of packets + // By default ttl=64 + else if (param.getName().equalsIgnoreCase("ttl")) { + if (param.getValue()!=null) { + try { + int ttl = Integer.parseInt(param.getValue()); + if (ttl<0) throw new IllegalStateException(); + builder.setTimeToLive(ttl); + } catch (Exception e) { + throw new IllegalStateException("The TTL must be a positive integer !"); + } + } + } + + // H.264 + else if (param.getName().equalsIgnoreCase("h264")) { + VideoQuality quality = VideoQuality.parseQuality(param.getValue()); + builder.setVideoQuality(quality).setVideoEncoder(VIDEO_H264); + } + + // H.263 + else if (param.getName().equalsIgnoreCase("h263")) { + VideoQuality quality = VideoQuality.parseQuality(param.getValue()); + builder.setVideoQuality(quality).setVideoEncoder(VIDEO_H263); + } + + // AMR + else if (param.getName().equalsIgnoreCase("amrnb") || param.getName().equalsIgnoreCase("amr")) { + AudioQuality quality = AudioQuality.parseQuality(param.getValue()); + builder.setAudioQuality(quality).setAudioEncoder(AUDIO_AMRNB); + } + + // AAC + else if (param.getName().equalsIgnoreCase("aac")) { + AudioQuality quality = AudioQuality.parseQuality(param.getValue()); + builder.setAudioQuality(quality).setAudioEncoder(AUDIO_AAC); + } + + } + + } + + if (builder.getVideoEncoder()==VIDEO_NONE && builder.getAudioEncoder()==AUDIO_NONE) { + SessionBuilder b = SessionBuilder.getInstance(); + builder.setVideoEncoder(b.getVideoEncoder()); + builder.setAudioEncoder(b.getAudioEncoder()); + } + + Session session = builder.build(); + + if (videoApi>0 && session.getVideoTrack() != null) { + session.getVideoTrack().setStreamingMethod(videoApi); + } + + if (audioApi>0 && session.getAudioTrack() != null) { + session.getAudioTrack().setStreamingMethod(audioApi); + } + + return session; + + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/video/CodecManager.java b/src/main/java/net/majorkernelpanic/streaming/video/CodecManager.java new file mode 100644 index 00000000..0d9d4cc8 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/video/CodecManager.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2011-2013 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.video; + +import java.util.ArrayList; +import java.util.HashMap; + +import android.annotation.SuppressLint; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.os.Build; +import android.util.Log; +import android.util.SparseArray; + +@SuppressLint("InlinedApi") +public class CodecManager { + + public final static String TAG = "CodecManager"; + + public static final int[] SUPPORTED_COLOR_FORMATS = { + MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar, + MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar + }; + + /** + * There currently is no way to know if an encoder is software or hardware from the MediaCodecInfo class, + * so we need to maintain a list of known software encoders. + */ + public static final String[] SOFTWARE_ENCODERS = { + "OMX.google.h264.encoder" + }; + + /** + * Contains a list of encoders and color formats that we may use with a {@link CodecManager.Translator}. + */ + static class Codecs { + /** A hardware encoder supporting a color format we can use. */ + public String hardwareCodec; + public int hardwareColorFormat; + /** A software encoder supporting a color format we can use. */ + public String softwareCodec; + public int softwareColorFormat; + } + + /** + * Contains helper functions to choose an encoder and a color format. + */ + static class Selector { + + private static HashMap>> sHardwareCodecs = new HashMap>>(); + private static HashMap>> sSoftwareCodecs = new HashMap>>(); + + /** + * Determines the most appropriate encoder to compress the video from the Camera + */ + public static Codecs findCodecsFormMimeType(String mimeType, boolean tryColorFormatSurface) { + findSupportedColorFormats(mimeType); + SparseArray> hardwareCodecs = sHardwareCodecs.get(mimeType); + SparseArray> softwareCodecs = sSoftwareCodecs.get(mimeType); + Codecs list = new Codecs(); + + // On devices running 4.3, we need an encoder supporting the color format used to work with a Surface + if (Build.VERSION.SDK_INT>=18 && tryColorFormatSurface) { + int colorFormatSurface = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; + try { + // We want a hardware encoder + list.hardwareCodec = hardwareCodecs.get(colorFormatSurface).get(0); + list.hardwareColorFormat = colorFormatSurface; + } catch (Exception e) {} + try { + // We want a software encoder + list.softwareCodec = softwareCodecs.get(colorFormatSurface).get(0); + list.softwareColorFormat = colorFormatSurface; + } catch (Exception e) {} + + if (list.hardwareCodec != null) { + Log.v(TAG,"Choosen primary codec: "+list.hardwareCodec+" with color format: "+list.hardwareColorFormat); + } else { + Log.e(TAG,"No supported hardware codec found !"); + } + if (list.softwareCodec != null) { + Log.v(TAG,"Choosen secondary codec: "+list.hardwareCodec+" with color format: "+list.hardwareColorFormat); + } else { + Log.e(TAG,"No supported software codec found !"); + } + return list; + } + + for (int i=0;i> softwareCodecs = new SparseArray>(); + SparseArray> hardwareCodecs = new SparseArray>(); + + if (sSoftwareCodecs.containsKey(mimeType)) { + return; + } + + Log.v(TAG,"Searching supported color formats for mime type \""+mimeType+"\"..."); + + // We loop through the encoders, apparently this can take up to a sec (testes on a GS3) + for(int j = MediaCodecList.getCodecCount() - 1; j >= 0; j--){ + MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(j); + if (!codecInfo.isEncoder()) continue; + + String[] types = codecInfo.getSupportedTypes(); + for (int i = 0; i < types.length; i++) { + if (types[i].equalsIgnoreCase(mimeType)) { + MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType); + + boolean software = false; + for (int k=0;k()); + softwareCodecs.get(format).add(codecInfo.getName()); + } else { + if (hardwareCodecs.get(format) == null) hardwareCodecs.put(format, new ArrayList()); + hardwareCodecs.get(format).add(codecInfo.getName()); + } + } + + } + } + } + + // Logs the supported color formats on the phone + StringBuilder e = new StringBuilder(); + e.append("Supported color formats on this phone: "); + for (int i=0;i=640) { + // Using the MediaCodec API with the buffer method for high resolutions is too slow + mMode = MODE_MEDIARECORDER_API; + } + EncoderDebugger debugger = EncoderDebugger.debug(mSettings, mQuality.resX, mQuality.resY); + return new MP4Config(debugger.getB64SPS(), debugger.getB64PPS()); + } catch (Exception e) { + // Fallback on the old streaming method using the MediaRecorder API + Log.e(TAG,"Resolution not supported with the MediaCodec API, we fallback on the old streamign method."); + mMode = MODE_MEDIARECORDER_API; + return testH264(); + } + } + + // Should not be called by the UI thread + private MP4Config testMediaRecorderAPI() throws RuntimeException, IOException { + String key = PREF_PREFIX+"h264-mr-"+mRequestedQuality.framerate+","+mRequestedQuality.resX+","+mRequestedQuality.resY; + + if (mSettings != null) { + if (mSettings.contains(key)) { + String[] s = mSettings.getString(key, "").split(","); + return new MP4Config(s[0],s[1],s[2]); + } + } + + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + throw new StorageUnavailableException("No external storage or external storage not ready !"); + } + + final String TESTFILE = Environment.getExternalStorageDirectory().getPath()+"/spydroid-test.mp4"; + + Log.i(TAG,"Testing H264 support... Test file saved at: "+TESTFILE); + + try { + File file = new File(TESTFILE); + file.createNewFile(); + } catch (IOException e) { + throw new StorageUnavailableException(e.getMessage()); + } + + // Save flash state & set it to false so that led remains off while testing h264 + boolean savedFlashState = mFlashEnabled; + mFlashEnabled = false; + + boolean previewStarted = mPreviewStarted; + + boolean cameraOpen = mCamera!=null; + createCamera(); + + // Stops the preview if needed + if (mPreviewStarted) { + lockCamera(); + try { + mCamera.stopPreview(); + } catch (Exception e) {} + mPreviewStarted = false; + } + + try { + Thread.sleep(100); + } catch (InterruptedException e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } + + unlockCamera(); + + try { + + mMediaRecorder = new MediaRecorder(); + mMediaRecorder.setCamera(mCamera); + mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); + mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); + mMediaRecorder.setVideoEncoder(mVideoEncoder); + mMediaRecorder.setPreviewDisplay(mSurfaceView.getHolder().getSurface()); + mMediaRecorder.setVideoSize(mRequestedQuality.resX,mRequestedQuality.resY); + mMediaRecorder.setVideoFrameRate(mRequestedQuality.framerate); + mMediaRecorder.setVideoEncodingBitRate((int)(mRequestedQuality.bitrate*0.8)); + mMediaRecorder.setOutputFile(TESTFILE); + mMediaRecorder.setMaxDuration(3000); + + // We wait a little and stop recording + mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { + public void onInfo(MediaRecorder mr, int what, int extra) { + Log.d(TAG,"MediaRecorder callback called !"); + if (what==MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) { + Log.d(TAG,"MediaRecorder: MAX_DURATION_REACHED"); + } else if (what==MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { + Log.d(TAG,"MediaRecorder: MAX_FILESIZE_REACHED"); + } else if (what==MediaRecorder.MEDIA_RECORDER_INFO_UNKNOWN) { + Log.d(TAG,"MediaRecorder: INFO_UNKNOWN"); + } else { + Log.d(TAG,"WTF ?"); + } + mLock.release(); + } + }); + + // Start recording + mMediaRecorder.prepare(); + mMediaRecorder.start(); + + if (mLock.tryAcquire(6,TimeUnit.SECONDS)) { + Log.d(TAG,"MediaRecorder callback was called :)"); + Thread.sleep(400); + } else { + Log.d(TAG,"MediaRecorder callback was not called after 6 seconds... :("); + } + } catch (IOException e) { + throw new ConfNotSupportedException(e.getMessage()); + } catch (RuntimeException e) { + throw new ConfNotSupportedException(e.getMessage()); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + try { + mMediaRecorder.stop(); + } catch (Exception e) {} + mMediaRecorder.release(); + mMediaRecorder = null; + lockCamera(); + if (!cameraOpen) destroyCamera(); + // Restore flash state + mFlashEnabled = savedFlashState; + if (previewStarted) { + // If the preview was started before the test, we try to restart it. + try { + startPreview(); + } catch (Exception e) {} + } + } + + // Retrieve SPS & PPS & ProfileId with MP4Config + MP4Config config = new MP4Config(TESTFILE); + + // Delete dummy video + File file = new File(TESTFILE); + if (!file.delete()) Log.e(TAG,"Temp file could not be erased"); + + Log.i(TAG,"H264 Test succeded..."); + + // Save test result + if (mSettings != null) { + Editor editor = mSettings.edit(); + editor.putString(key, config.getProfileLevel()+","+config.getB64SPS()+","+config.getB64PPS()); + editor.commit(); + } + + return config; + + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/video/VideoQuality.java b/src/main/java/net/majorkernelpanic/streaming/video/VideoQuality.java new file mode 100644 index 00000000..8c857f86 --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/video/VideoQuality.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.video; + +import java.util.Iterator; +import java.util.List; + +import android.hardware.Camera; +import android.hardware.Camera.Size; +import android.util.Log; + +/** + * A class that represents the quality of a video stream. + * It contains the resolution, the framerate (in fps) and the bitrate (in bps) of the stream. + */ +public class VideoQuality { + + public final static String TAG = "VideoQuality"; + + /** Default video stream quality. */ + public final static VideoQuality DEFAULT_VIDEO_QUALITY = new VideoQuality(176,144,20,500000); + + /** Represents a quality for a video stream. */ + public VideoQuality() {} + + /** + * Represents a quality for a video stream. + * @param resX The horizontal resolution + * @param resY The vertical resolution + */ + public VideoQuality(int resX, int resY) { + this.resX = resX; + this.resY = resY; + } + + /** + * Represents a quality for a video stream. + * @param resX The horizontal resolution + * @param resY The vertical resolution + * @param framerate The framerate in frame per seconds + * @param bitrate The bitrate in bit per seconds + */ + public VideoQuality(int resX, int resY, int framerate, int bitrate) { + this.framerate = framerate; + this.bitrate = bitrate; + this.resX = resX; + this.resY = resY; + } + + public int framerate = 0; + public int bitrate = 0; + public int resX = 0; + public int resY = 0; + + public boolean equals(VideoQuality quality) { + if (quality==null) return false; + return (quality.resX == this.resX & + quality.resY == this.resY & + quality.framerate == this.framerate & + quality.bitrate == this.bitrate); + } + + public VideoQuality clone() { + return new VideoQuality(resX,resY,framerate,bitrate); + } + + public static VideoQuality parseQuality(String str) { + VideoQuality quality = DEFAULT_VIDEO_QUALITY.clone(); + if (str != null) { + String[] config = str.split("-"); + try { + quality.bitrate = Integer.parseInt(config[0])*1000; // conversion to bit/s + quality.framerate = Integer.parseInt(config[1]); + quality.resX = Integer.parseInt(config[2]); + quality.resY = Integer.parseInt(config[3]); + } + catch (IndexOutOfBoundsException ignore) {} + } + return quality; + } + + public String toString() { + return resX+"x"+resY+" px, "+framerate+" fps, "+bitrate/1000+" kbps"; + } + + /** + * Checks if the requested resolution is supported by the camera. + * If not, it modifies it by supported parameters. + **/ + public static VideoQuality determineClosestSupportedResolution(Camera.Parameters parameters, VideoQuality quality) { + VideoQuality v = quality.clone(); + int minDist = Integer.MAX_VALUE; + String supportedSizesStr = "Supported resolutions: "; + List supportedSizes = parameters.getSupportedPreviewSizes(); + for (Iterator it = supportedSizes.iterator(); it.hasNext();) { + Size size = it.next(); + supportedSizesStr += size.width+"x"+size.height+(it.hasNext()?", ":""); + int dist = Math.abs(quality.resX - size.width); + if (dist"+v.resX+"x"+v.resY); + } + + return v; + } + + public static int[] determineMaximumSupportedFramerate(Camera.Parameters parameters) { + int[] maxFps = new int[]{0,0}; + String supportedFpsRangesStr = "Supported frame rates: "; + List supportedFpsRanges = parameters.getSupportedPreviewFpsRange(); + for (Iterator it = supportedFpsRanges.iterator(); it.hasNext();) { + int[] interval = it.next(); + // Intervals are returned as integers, for example "29970" means "29.970" FPS. + supportedFpsRangesStr += interval[0]/1000+"-"+interval[1]/1000+"fps"+(it.hasNext()?", ":""); + if (interval[1]>maxFps[1] || (interval[0]>maxFps[0] && interval[1]==maxFps[1])) { + maxFps = interval; + } + } + Log.v(TAG,supportedFpsRangesStr); + return maxFps; + } + +} diff --git a/src/main/java/net/majorkernelpanic/streaming/video/VideoStream.java b/src/main/java/net/majorkernelpanic/streaming/video/VideoStream.java new file mode 100644 index 00000000..165fb8db --- /dev/null +++ b/src/main/java/net/majorkernelpanic/streaming/video/VideoStream.java @@ -0,0 +1,729 @@ +/* + * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com + * + * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) + * + * Spydroid is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package net.majorkernelpanic.streaming.video; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import net.majorkernelpanic.streaming.MediaStream; +import net.majorkernelpanic.streaming.Stream; +import net.majorkernelpanic.streaming.exceptions.CameraInUseException; +import net.majorkernelpanic.streaming.exceptions.ConfNotSupportedException; +import net.majorkernelpanic.streaming.exceptions.InvalidSurfaceException; +import net.majorkernelpanic.streaming.gl.SurfaceView; +import net.majorkernelpanic.streaming.hw.EncoderDebugger; +import net.majorkernelpanic.streaming.hw.NV21Convertor; +import net.majorkernelpanic.streaming.rtp.MediaCodecInputStream; +import android.annotation.SuppressLint; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.media.MediaRecorder; +import android.os.Looper; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceHolder.Callback; + +/** + * Don't use this class directly. + */ +public abstract class VideoStream extends MediaStream { + + protected final static String TAG = "VideoStream"; + + protected VideoQuality mRequestedQuality = VideoQuality.DEFAULT_VIDEO_QUALITY.clone(); + protected VideoQuality mQuality = mRequestedQuality.clone(); + protected SurfaceHolder.Callback mSurfaceHolderCallback = null; + protected SurfaceView mSurfaceView = null; + protected SharedPreferences mSettings = null; + protected int mVideoEncoder, mCameraId = 0; + protected int mRequestedOrientation = 0, mOrientation = 0; + protected Camera mCamera; + protected Thread mCameraThread; + protected Looper mCameraLooper; + + protected boolean mCameraOpenedManually = true; + protected boolean mFlashEnabled = false; + protected boolean mSurfaceReady = false; + protected boolean mUnlocked = false; + protected boolean mPreviewStarted = false; + protected boolean mUpdated = false; + + protected String mMimeType; + protected String mEncoderName; + protected int mEncoderColorFormat; + protected int mCameraImageFormat; + protected int mMaxFps = 0; + + /** + * Don't use this class directly. + * Uses CAMERA_FACING_BACK by default. + */ + public VideoStream() { + this(CameraInfo.CAMERA_FACING_BACK); + } + + /** + * Don't use this class directly + * @param camera Can be either CameraInfo.CAMERA_FACING_BACK or CameraInfo.CAMERA_FACING_FRONT + */ + @SuppressLint("InlinedApi") + public VideoStream(int camera) { + super(); + setCamera(camera); + } + + /** + * Sets the camera that will be used to capture video. + * You can call this method at any time and changes will take effect next time you start the stream. + * @param camera Can be either CameraInfo.CAMERA_FACING_BACK or CameraInfo.CAMERA_FACING_FRONT + */ + public void setCamera(int camera) { + CameraInfo cameraInfo = new CameraInfo(); + int numberOfCameras = Camera.getNumberOfCameras(); + for (int i=0;i3) { + i = 0; + //Log.d(TAG,"Measured: "+1000000L/(now-oldnow)+" fps."); + } + try { + int bufferIndex = mMediaCodec.dequeueInputBuffer(500000); + if (bufferIndex>=0) { + inputBuffers[bufferIndex].clear(); + if (data == null) Log.e(TAG,"Symptom of the \"Callback buffer was to small\" problem..."); + else convertor.convert(data, inputBuffers[bufferIndex]); + mMediaCodec.queueInputBuffer(bufferIndex, 0, inputBuffers[bufferIndex].position(), now, 0); + } else { + Log.e(TAG,"No buffer available !"); + } + } finally { + mCamera.addCallbackBuffer(data); + } + } + }; + + for (int i=0;i<10;i++) mCamera.addCallbackBuffer(new byte[convertor.getBufferSize()]); + mCamera.setPreviewCallbackWithBuffer(callback); + + // 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; + + } + + /** + * Video encoding is done by a MediaCodec. + * But here we will use the buffer-to-surface method + */ + @SuppressLint({ "InlinedApi", "NewApi" }) + protected void encodeWithMediaCodecMethod2() throws RuntimeException, IOException { + + Log.d(TAG,"Video encoded using the MediaCodec API with a surface"); + + // Updates the parameters of the camera if needed + createCamera(); + updateCamera(); + + // Estimates the framerate of the camera + measureFramerate(); + + EncoderDebugger debugger = EncoderDebugger.debug(mSettings, mQuality.resX, mQuality.resY); + + mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName()); + MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mQuality.resX, mQuality.resY); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mQuality.bitrate); + mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mQuality.framerate); + mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); + mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + Surface surface = mMediaCodec.createInputSurface(); + ((SurfaceView)mSurfaceView).addMediaCodecSurface(surface); + mMediaCodec.start(); + + // 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; + + } + + /** + * Returns a description of the stream using SDP. + * This method can only be called after {@link Stream#configure()}. + * @throws IllegalStateException Thrown when {@link Stream#configure()} wa not called. + */ + public abstract String getSessionDescription() throws IllegalStateException; + + /** + * Opens the camera in a new Looper thread so that the preview callback is not called from the main thread + * If an exception is thrown in this Looper thread, we bring it back into the main thread. + * @throws RuntimeException Might happen if another app is already using the camera. + */ + private void openCamera() throws RuntimeException { + final Semaphore lock = new Semaphore(0); + final RuntimeException[] exception = new RuntimeException[1]; + mCameraThread = new Thread(new Runnable() { + @Override + public void run() { + Looper.prepare(); + mCameraLooper = Looper.myLooper(); + try { + mCamera = Camera.open(mCameraId); + } catch (RuntimeException e) { + exception[0] = e; + } finally { + lock.release(); + Looper.loop(); + } + } + }); + mCameraThread.start(); + lock.acquireUninterruptibly(); + if (exception[0] != null) throw new CameraInUseException(exception[0].getMessage()); + } + + protected synchronized void createCamera() throws RuntimeException { + if (mSurfaceView == null) + throw new InvalidSurfaceException("Invalid surface !"); + if (mSurfaceView.getHolder() == null || !mSurfaceReady) + throw new InvalidSurfaceException("Invalid surface !"); + + if (mCamera == null) { + openCamera(); + mUpdated = false; + mUnlocked = false; + mCamera.setErrorCallback(new Camera.ErrorCallback() { + @Override + public void onError(int error, Camera camera) { + // On some phones when trying to use the camera facing front the media server will die + // Whether or not this callback may be called really depends on the phone + if (error == Camera.CAMERA_ERROR_SERVER_DIED) { + // In this case the application must release the camera and instantiate a new one + Log.e(TAG,"Media server died !"); + // We don't know in what thread we are so stop needs to be synchronized + mCameraOpenedManually = false; + stop(); + } else { + Log.e(TAG,"Error unknown with the camera: "+error); + } + } + }); + + try { + + // If the phone has a flash, we turn it on/off according to mFlashEnabled + // setRecordingHint(true) is a very nice optimization if you plane to only use the Camera for recording + Parameters parameters = mCamera.getParameters(); + if (parameters.getFlashMode()!=null) { + parameters.setFlashMode(mFlashEnabled?Parameters.FLASH_MODE_TORCH:Parameters.FLASH_MODE_OFF); + } + parameters.setRecordingHint(true); + mCamera.setParameters(parameters); + mCamera.setDisplayOrientation(mOrientation); + + try { + if (mMode == MODE_MEDIACODEC_API_2) { + mSurfaceView.startGLThread(); + mCamera.setPreviewTexture(mSurfaceView.getSurfaceTexture()); + } else { + mCamera.setPreviewDisplay(mSurfaceView.getHolder()); + } + } catch (IOException e) { + throw new InvalidSurfaceException("Invalid surface !"); + } + + } catch (RuntimeException e) { + destroyCamera(); + throw e; + } + + } + } + + protected synchronized void destroyCamera() { + if (mCamera != null) { + if (mStreaming) super.stop(); + lockCamera(); + mCamera.stopPreview(); + try { + mCamera.release(); + } catch (Exception e) { + Log.e(TAG,e.getMessage()!=null?e.getMessage():"unknown error"); + } + mCamera = null; + mCameraLooper.quit(); + mUnlocked = false; + mPreviewStarted = false; + } + } + + protected synchronized void updateCamera() throws RuntimeException { + + // The camera is already correctly configured + if (mUpdated) return; + + if (mPreviewStarted) { + mPreviewStarted = false; + mCamera.stopPreview(); + } + + Parameters parameters = mCamera.getParameters(); + mQuality = VideoQuality.determineClosestSupportedResolution(parameters, mQuality); + int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters); + + double ratio = (double)mQuality.resX/(double)mQuality.resY; + mSurfaceView.requestAspectRatio(ratio); + + parameters.setPreviewFormat(mCameraImageFormat); + parameters.setPreviewSize(mQuality.resX, mQuality.resY); + parameters.setPreviewFpsRange(max[0], max[1]); + + try { + mCamera.setParameters(parameters); + mCamera.setDisplayOrientation(mOrientation); + mCamera.startPreview(); + mPreviewStarted = true; + mUpdated = true; + } catch (RuntimeException e) { + destroyCamera(); + throw e; + } + } + + protected void lockCamera() { + if (mUnlocked) { + Log.d(TAG,"Locking camera"); + try { + mCamera.reconnect(); + } catch (Exception e) { + Log.e(TAG,e.getMessage()); + } + mUnlocked = false; + } + } + + protected void unlockCamera() { + if (!mUnlocked) { + Log.d(TAG,"Unlocking camera"); + try { + mCamera.unlock(); + } catch (Exception e) { + Log.e(TAG,e.getMessage()); + } + mUnlocked = true; + } + } + + + /** + * Computes the average frame rate at which the preview callback is called. + * We will then use this average framerate with the MediaCodec. + * Blocks the thread in which this function is called. + */ + private void measureFramerate() { + final Semaphore lock = new Semaphore(0); + + final Camera.PreviewCallback callback = new Camera.PreviewCallback() { + int i = 0, t = 0; + long now, oldnow, count = 0; + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + i++; + now = System.nanoTime()/1000; + if (i>3) { + t += now - oldnow; + count++; + } + if (i>20) { + mQuality.framerate = (int) (1000000/(t/count)+1); + lock.release(); + } + oldnow = now; + } + }; + + mCamera.setPreviewCallback(callback); + + try { + lock.tryAcquire(2,TimeUnit.SECONDS); + Log.d(TAG,"Actual framerate: "+mQuality.framerate); + if (mSettings != null) { + Editor editor = mSettings.edit(); + editor.putInt(PREF_PREFIX+"fps"+mRequestedQuality.framerate+","+mCameraImageFormat+","+mRequestedQuality.resX+mRequestedQuality.resY, mQuality.framerate); + editor.commit(); + } + } catch (InterruptedException e) {} + + mCamera.setPreviewCallback(null); + + } + +} From 93a109455719ab0c47506722d8c9eee46d228fbc Mon Sep 17 00:00:00 2001 From: prakarnwongsanit Date: Thu, 16 Apr 2015 16:10:52 +0700 Subject: [PATCH 3/3] Add unuse files to gitignore --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 12f6ddb2..3f7d7d59 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,8 @@ gen/ local.properties custom_rules.xml ant.properties -*~ \ No newline at end of file +*~ +.gradle/ +.idea/ +build/ +*.iml