├── .gitignore ├── .idea ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── zpf │ │ └── androidshow │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── zpf │ │ │ └── androidshow │ │ │ ├── MainActivity.java │ │ │ ├── media │ │ │ ├── H264Helper.java │ │ │ ├── MediaCodecBase.java │ │ │ ├── VideoMediaCodec.java │ │ │ └── h264data.java │ │ │ ├── rtsp │ │ │ ├── AbstractPacketizer.java │ │ │ ├── AudioQuality.java │ │ │ ├── AudioStream.java │ │ │ ├── H264Packetizer.java │ │ │ ├── MediaStream.java │ │ │ ├── RtpSocket.java │ │ │ ├── RtspServer.java │ │ │ ├── ScreenInputStream.java │ │ │ ├── ScreenStream.java │ │ │ ├── SenderReport.java │ │ │ ├── Session.java │ │ │ ├── SessionBuilder.java │ │ │ ├── Stream.java │ │ │ ├── UriParser.java │ │ │ ├── VideoQuality.java │ │ │ └── VideoStream.java │ │ │ └── screen │ │ │ ├── Constant.java │ │ │ └── ScreenRecord.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── zpf │ └── androidshow │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidShow 2 | 通过MediaProjectionManager采集android屏幕视频流,app中搭建简单rtsp server与客户端通信,通过rtp 协议传输视频流,vlc等客户端来观看手机的屏幕实时视频 3 | 4 | 2019-8-20 5 | 6 | 增加动态权限申请 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | defaultConfig { 6 | applicationId "com.zpf.androidshow" 7 | minSdkVersion 21 8 | targetSdkVersion 26 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | useLibrary 'org.apache.http.legacy' 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(dir: 'libs', include: ['*.jar']) 24 | implementation 'com.android.support:appcompat-v7:26.1.0' 25 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 26 | testImplementation 'junit:junit:4.12' 27 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 28 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/zpf/androidshow/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.zpf.androidshow; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.zpf.androidshow", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.zpf.androidshow; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.content.Intent; 7 | import android.content.ServiceConnection; 8 | import android.content.pm.PackageManager; 9 | import android.hardware.display.DisplayManager; 10 | import android.hardware.display.VirtualDisplay; 11 | import android.media.projection.MediaProjection; 12 | import android.media.projection.MediaProjectionManager; 13 | import android.net.wifi.WifiInfo; 14 | import android.net.wifi.WifiManager; 15 | import android.os.Build; 16 | import android.os.IBinder; 17 | import android.support.v4.app.ActivityCompat; 18 | import android.support.v4.content.ContextCompat; 19 | import android.support.v7.app.AlertDialog; 20 | import android.support.v7.app.AppCompatActivity; 21 | import android.os.Bundle; 22 | import android.view.TextureView; 23 | import android.view.View; 24 | import android.widget.Button; 25 | import android.widget.TextView; 26 | import android.widget.Toast; 27 | 28 | import com.zpf.androidshow.media.h264data; 29 | import com.zpf.androidshow.rtsp.RtspServer; 30 | import com.zpf.androidshow.screen.Constant; 31 | import com.zpf.androidshow.screen.ScreenRecord; 32 | 33 | import java.util.Locale; 34 | import java.util.concurrent.ArrayBlockingQueue; 35 | 36 | /** 37 | * Created by zpf on 2018/3/6 38 | * 39 | * MediaProjectionManager流程 40 | * 1.调用Context.getSystemService()方法即可获取MediaProjectionManager实例 41 | * 2.调用MediaProjectionManager对象的createScreenCaptureIntent()方法创建一个屏幕捕捉的Intent 42 | * 3.调用startActivityForResult()方法启动第2步得到的Intent,这样即可启动屏幕捕捉的Intent 43 | * 4.重写onActivityResult()方法,在该方法中通过MediaProjectionManager对象来获取MediaProjection对象,在该对象中即可获取被捕获的屏幕 44 | * **/ 45 | public class MainActivity extends AppCompatActivity implements View.OnClickListener{ 46 | 47 | public static final int REQUEST_CODE_A = 10001; 48 | 49 | private Button start_record,stop_record; 50 | 51 | private TextView line2; 52 | 53 | private MediaProjectionManager mMediaProjectionManager; 54 | 55 | private ScreenRecord mScreenRecord; 56 | 57 | private boolean isRecording = false; 58 | 59 | private static int queuesize = 30; 60 | public static ArrayBlockingQueue h264Queue = new ArrayBlockingQueue<>(queuesize); 61 | private RtspServer mRtspServer; 62 | private String RtspAddress; 63 | private final static int PERMISSIONS_OK = 10001; 64 | private static String[] PERMISSIONS_STORAGE = { 65 | "android.permission.RECORD_AUDIO", 66 | "android.permission.CAMERA", 67 | "android.permission.READ_EXTERNAL_STORAGE", 68 | "android.permission.WRITE_EXTERNAL_STORAGE" }; 69 | 70 | @Override 71 | protected void onCreate(Bundle savedInstanceState) { 72 | super.onCreate(savedInstanceState); 73 | setContentView(R.layout.activity_main); 74 | InitView(); 75 | if (Build.VERSION.SDK_INT>22) { 76 | if (!checkPermissionAllGranted(PERMISSIONS_STORAGE)) { 77 | //先判断有没有权限 ,没有就在这里进行权限的申请 78 | ActivityCompat.requestPermissions(MainActivity.this, 79 | PERMISSIONS_STORAGE, PERMISSIONS_OK); 80 | }else{ 81 | init(); 82 | } 83 | }else{ 84 | init(); 85 | } 86 | } 87 | 88 | @Override 89 | public void onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults) { 90 | switch (requestCode) { 91 | case PERMISSIONS_OK: 92 | if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 93 | //这里已经获取到了摄像头的权限,想干嘛干嘛了可以 94 | init(); 95 | } else { 96 | showWaringDialog(); 97 | } 98 | break; 99 | default: 100 | break; 101 | } 102 | } 103 | 104 | private void showWaringDialog() { 105 | android.app.AlertDialog dialog = new android.app.AlertDialog.Builder(this) 106 | .setTitle("警告!") 107 | .setMessage("请前往设置->应用->PermissionDemo->权限中打开相关权限,否则功能无法正常运行!") 108 | .setPositiveButton("确定", new DialogInterface.OnClickListener() { 109 | @Override 110 | public void onClick(DialogInterface dialog, int which) { 111 | // 一般情况下如果用户不授权的话,功能是无法运行的,做退出处理 112 | finish(); 113 | } 114 | }).show(); 115 | } 116 | 117 | private void init(){ 118 | InitMPManager(); 119 | RtspAddress = displayIpAddress(); 120 | if(RtspAddress != null){ 121 | line2.setText(RtspAddress); 122 | } 123 | } 124 | 125 | private boolean checkPermissionAllGranted(String[] permissions) { 126 | for (String permission : permissions) { 127 | if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { 128 | // 只要有一个权限没有被授予, 则直接返回 false 129 | return false; 130 | } 131 | } 132 | return true; 133 | } 134 | 135 | private ServiceConnection mRtspServiceConnection = new ServiceConnection() { 136 | 137 | @Override 138 | public void onServiceConnected(ComponentName name, IBinder service) { 139 | mRtspServer = ((RtspServer.LocalBinder)service).getService(); 140 | mRtspServer.addCallbackListener(mRtspCallbackListener); 141 | mRtspServer.start(); 142 | } 143 | 144 | @Override 145 | public void onServiceDisconnected(ComponentName name) {} 146 | }; 147 | 148 | @Override 149 | protected void onResume(){ 150 | super.onResume(); 151 | } 152 | 153 | private RtspServer.CallbackListener mRtspCallbackListener = new RtspServer.CallbackListener() { 154 | 155 | @Override 156 | public void onError(RtspServer server, Exception e, int error) { 157 | // We alert the user that the port is already used by another app. 158 | if (error == RtspServer.ERROR_BIND_FAILED) { 159 | new AlertDialog.Builder(MainActivity.this) 160 | .setTitle("Port already in use !") 161 | .setMessage("You need to choose another port for the RTSP server !") 162 | .show(); 163 | } 164 | } 165 | 166 | @Override 167 | public void onMessage(RtspServer server, int message) { 168 | if (message==RtspServer.MESSAGE_STREAMING_STARTED) { 169 | runOnUiThread(new Runnable() { 170 | public void run() { 171 | Toast.makeText(MainActivity.this,"RTSP STREAM STARTED",Toast.LENGTH_SHORT).show(); 172 | } 173 | }); 174 | } else if (message==RtspServer.MESSAGE_STREAMING_STOPPED) { 175 | runOnUiThread(new Runnable() { 176 | public void run() { 177 | Toast.makeText(MainActivity.this,"RTSP STREAM STOPPED",Toast.LENGTH_SHORT).show(); 178 | } 179 | }); 180 | } 181 | } 182 | }; 183 | 184 | 185 | public static void putData(byte[] buffer, int type,long ts) { 186 | if (h264Queue.size() >= queuesize) { 187 | h264Queue.poll(); 188 | } 189 | h264data data = new h264data(); 190 | data.data = buffer; 191 | data.type = type; 192 | data.ts = ts; 193 | h264Queue.add(data); 194 | } 195 | 196 | /** 197 | * 初始化View 198 | * **/ 199 | private void InitView(){ 200 | start_record = findViewById(R.id.start_record); 201 | start_record.setOnClickListener(this); 202 | stop_record = findViewById(R.id.stop_record); 203 | stop_record.setOnClickListener(this); 204 | line2 = (TextView)findViewById(R.id.line2); 205 | } 206 | 207 | /** 208 | * 初始化MediaProjectionManager 209 | * **/ 210 | private void InitMPManager(){ 211 | mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE); 212 | } 213 | 214 | 215 | /** 216 | * 开始截屏 217 | * **/ 218 | private void StartScreenCapture(){ 219 | if(RtspAddress != null && !RtspAddress.isEmpty()){ 220 | isRecording = true; 221 | Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent(); 222 | startActivityForResult(captureIntent, REQUEST_CODE_A); 223 | bindService(new Intent(this,RtspServer.class), mRtspServiceConnection, Context.BIND_AUTO_CREATE); 224 | }else{ 225 | Toast.makeText(this,"网络连接异常!",Toast.LENGTH_SHORT).show(); 226 | } 227 | } 228 | 229 | 230 | /** 231 | * 停止截屏 232 | * **/ 233 | private void StopScreenCapture(){ 234 | isRecording = false; 235 | mScreenRecord.release(); 236 | if (mRtspServer != null) 237 | mRtspServer.removeCallbackListener(mRtspCallbackListener); 238 | unbindService(mRtspServiceConnection); 239 | } 240 | 241 | 242 | /** 243 | * 244 | * **/ 245 | @Override 246 | protected void onActivityResult(int requestCode, int resultCode, Intent data){ 247 | try { 248 | MediaProjection mediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data); 249 | if(mediaProjection == null){ 250 | Toast.makeText(this,"程序发生错误:MediaProjection@1",Toast.LENGTH_SHORT).show(); 251 | return; 252 | } 253 | mScreenRecord = new ScreenRecord(this,mediaProjection); 254 | mScreenRecord.start(); 255 | } 256 | catch (Exception e){ 257 | 258 | } 259 | } 260 | 261 | 262 | @Override 263 | public void onClick(View view) { 264 | switch (view.getId()) 265 | { 266 | case R.id.start_record: 267 | StartScreenCapture(); 268 | break; 269 | case R.id.stop_record: 270 | StopScreenCapture(); 271 | break; 272 | } 273 | } 274 | 275 | 276 | /** 277 | * 先判断网络情况是否良好 278 | * */ 279 | private String displayIpAddress() { 280 | WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); 281 | WifiInfo info = wifiManager.getConnectionInfo(); 282 | String ipaddress = ""; 283 | if (info!=null && info.getNetworkId()>-1) { 284 | int i = info.getIpAddress(); 285 | String ip = String.format(Locale.ENGLISH,"%d.%d.%d.%d", i & 0xff, i >> 8 & 0xff,i >> 16 & 0xff,i >> 24 & 0xff); 286 | ipaddress += "rtsp://"; 287 | ipaddress += ip; 288 | ipaddress += ":"; 289 | ipaddress += RtspServer.DEFAULT_RTSP_PORT; 290 | } 291 | return ipaddress; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/media/H264Helper.java: -------------------------------------------------------------------------------- 1 | package com.zpf.androidshow.media; 2 | 3 | import android.media.MediaFormat; 4 | 5 | import java.nio.ByteBuffer; 6 | 7 | /** 8 | * Created by zpf on 2018/3/8. 9 | */ 10 | 11 | public class H264Helper { 12 | 13 | /** 14 | * 获取h264帧类型 15 | * I 0x05 16 | * NAL_SLICE 0x01 17 | * sps 0x07 18 | * pps 0x08 19 | * 20 | * **/ 21 | public static int getFrameType(Byte b){ 22 | if(b == null) 23 | return 1; 24 | return b & 0x1F; 25 | } 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/media/MediaCodecBase.java: -------------------------------------------------------------------------------- 1 | package com.zpf.androidshow.media; 2 | 3 | import android.media.MediaCodec; 4 | 5 | /** 6 | * Created by user111 on 2018/3/7. 7 | */ 8 | 9 | public abstract class MediaCodecBase { 10 | 11 | protected MediaCodec mEncoder; 12 | 13 | protected boolean isRun = false; 14 | 15 | public abstract void prepare(); 16 | 17 | public abstract void release(); 18 | 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/media/VideoMediaCodec.java: -------------------------------------------------------------------------------- 1 | package com.zpf.androidshow.media; 2 | 3 | import android.media.MediaCodec; 4 | import android.media.MediaCodecInfo; 5 | import android.media.MediaFormat; 6 | import android.os.Environment; 7 | import android.util.Log; 8 | import android.view.Surface; 9 | 10 | import com.zpf.androidshow.MainActivity; 11 | import com.zpf.androidshow.screen.Constant; 12 | 13 | import java.io.BufferedOutputStream; 14 | import java.io.File; 15 | import java.io.FileOutputStream; 16 | import java.io.IOException; 17 | import java.nio.ByteBuffer; 18 | 19 | /** 20 | * Created by zpf on 2018/3/7. 21 | */ 22 | 23 | public class VideoMediaCodec extends MediaCodecBase { 24 | 25 | private final static String TAG = "VideoMediaCodec"; 26 | 27 | 28 | private Surface mSurface; 29 | private long startTime = 0; 30 | private int TIMEOUT_USEC = 12000; 31 | public byte[] configbyte; 32 | 33 | private static String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test1.h264"; 34 | private BufferedOutputStream outputStream; 35 | FileOutputStream outStream; 36 | private void createfile(){ 37 | File file = new File(path); 38 | if(file.exists()){ 39 | file.delete(); 40 | } 41 | try { 42 | outputStream = new BufferedOutputStream(new FileOutputStream(file)); 43 | } catch (Exception e){ 44 | e.printStackTrace(); 45 | } 46 | } 47 | 48 | /** 49 | * 50 | * **/ 51 | public VideoMediaCodec(){ 52 | //createfile(); 53 | prepare(); 54 | } 55 | 56 | public Surface getSurface(){ 57 | return mSurface; 58 | } 59 | 60 | public void isRun(boolean isR){ 61 | this.isRun = isR; 62 | } 63 | 64 | 65 | @Override 66 | public void prepare(){ 67 | try{ 68 | MediaFormat format = MediaFormat.createVideoFormat(Constant.MIME_TYPE, Constant.VIDEO_WIDTH, Constant.VIDEO_HEIGHT); 69 | format.setInteger(MediaFormat.KEY_COLOR_FORMAT, 70 | MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); 71 | format.setInteger(MediaFormat.KEY_BIT_RATE, Constant.VIDEO_BITRATE); 72 | format.setInteger(MediaFormat.KEY_FRAME_RATE, Constant.VIDEO_FRAMERATE); 73 | format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, Constant.VIDEO_IFRAME_INTER); 74 | mEncoder = MediaCodec.createEncoderByType(Constant.MIME_TYPE); 75 | mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 76 | mSurface = mEncoder.createInputSurface(); 77 | mEncoder.start(); 78 | }catch (IOException e){ 79 | 80 | } 81 | } 82 | 83 | @Override 84 | public void release() { 85 | this.isRun = false; 86 | 87 | } 88 | 89 | 90 | /** 91 | * 获取h264数据 92 | * **/ 93 | public void getBuffer(){ 94 | try 95 | { 96 | while(isRun){ 97 | if(mEncoder == null) 98 | break; 99 | 100 | MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo(); 101 | int outputBufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); 102 | while (outputBufferIndex >= 0){ 103 | ByteBuffer outputBuffer = mEncoder.getOutputBuffers()[outputBufferIndex]; 104 | byte[] outData = new byte[mBufferInfo.size]; 105 | outputBuffer.get(outData); 106 | if(mBufferInfo.flags == 2){ 107 | configbyte = new byte[mBufferInfo.size]; 108 | configbyte = outData; 109 | } 110 | // else{ 111 | // MainActivity.putData(outData,2,mBufferInfo.presentationTimeUs*1000L); 112 | // } 113 | 114 | else if(mBufferInfo.flags == 1){ 115 | byte[] keyframe = new byte[mBufferInfo.size + configbyte.length]; 116 | System.arraycopy(configbyte, 0, keyframe, 0, configbyte.length); 117 | System.arraycopy(outData, 0, keyframe, configbyte.length, outData.length); 118 | MainActivity.putData(keyframe,1,mBufferInfo.presentationTimeUs*1000L); 119 | // if(outputStream != null){ 120 | // outputStream.write(keyframe, 0, keyframe.length); 121 | // } 122 | }else{ 123 | MainActivity.putData(outData,2,mBufferInfo.presentationTimeUs*1000L); 124 | // if(outputStream != null){ 125 | // outputStream.write(outData, 0, outData.length); 126 | // } 127 | } 128 | mEncoder.releaseOutputBuffer(outputBufferIndex, false); 129 | outputBufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); 130 | } 131 | } 132 | } 133 | catch (Exception e){ 134 | 135 | } 136 | try { 137 | mEncoder.stop(); 138 | mEncoder.release(); 139 | mEncoder = null; 140 | } catch (Exception e){ 141 | e.printStackTrace(); 142 | } 143 | // try { 144 | // outputStream.flush(); 145 | // outputStream.close(); 146 | // outputStream = null; 147 | // } catch (IOException e) { 148 | // e.printStackTrace(); 149 | // } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/media/h264data.java: -------------------------------------------------------------------------------- 1 | package com.zpf.androidshow.media; 2 | 3 | /** 4 | * Created by user111 on 2018/3/14. 5 | */ 6 | 7 | public class h264data { 8 | 9 | public byte[] data; 10 | 11 | public int type; 12 | 13 | public long ts; 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/AbstractPacketizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com 3 | * 4 | * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) 5 | * 6 | * Spydroid is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This source code is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this source code; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | package com.zpf.androidshow.rtsp; 22 | 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.net.InetAddress; 26 | import java.util.Random; 27 | 28 | /** 29 | * 30 | * Each packetizer inherits from this one and therefore uses RTP and UDP. 31 | * 32 | */ 33 | abstract public class AbstractPacketizer { 34 | 35 | protected static final int rtphl = RtpSocket.RTP_HEADER_LENGTH; 36 | 37 | // Maximum size of RTP packets 38 | protected final static int MAXPACKETSIZE = RtpSocket.MTU-28; 39 | 40 | protected RtpSocket socket = null; 41 | protected InputStream is = null; 42 | protected byte[] buffer; 43 | 44 | protected long ts = 0; 45 | 46 | public AbstractPacketizer() { 47 | int ssrc = new Random().nextInt(); 48 | ts = new Random().nextInt(); 49 | socket = new RtpSocket(); 50 | socket.setSSRC(ssrc); 51 | } 52 | 53 | public RtpSocket getRtpSocket() { 54 | return socket; 55 | } 56 | 57 | public SenderReport getRtcpSocket() { 58 | return socket.getRtcpSocket(); 59 | } 60 | 61 | 62 | public void setSSRC(int ssrc) { 63 | socket.setSSRC(ssrc); 64 | } 65 | 66 | public int getSSRC() { 67 | return socket.getSSRC(); 68 | } 69 | 70 | public void setInputStream(InputStream is) { 71 | this.is = is; 72 | } 73 | 74 | public void setTimeToLive(int ttl) throws IOException { 75 | socket.setTimeToLive(ttl); 76 | } 77 | 78 | /** 79 | * Sets the destination of the stream. 80 | * @param dest The destination address of the stream 81 | * @param rtpPort Destination port that will be used for RTP 82 | * @param rtcpPort Destination port that will be used for RTCP 83 | */ 84 | public void setDestination(InetAddress dest, int rtpPort, int rtcpPort) { 85 | socket.setDestination(dest, rtpPort, rtcpPort); 86 | } 87 | 88 | /** Starts the packetizer. */ 89 | public abstract void start(); 90 | 91 | /** Stops the packetizer. */ 92 | public abstract void stop(); 93 | 94 | /** Updates data for RTCP SR and sends the packet. */ 95 | protected void send(int length) throws IOException { 96 | socket.commitBuffer(length); 97 | } 98 | 99 | /** For debugging purposes. */ 100 | protected static String printBuffer(byte[] buffer, int start,int end) { 101 | String str = ""; 102 | for (int i=start;iperiod) { 137 | elapsed = 0; 138 | long now = System.nanoTime(); 139 | if (!initoffset || (now - start < 0)) { 140 | start = now; 141 | duration = 0; 142 | initoffset = true; 143 | } 144 | // Prevents drifting issues by comparing the real duration of the 145 | // stream with the sum of all temporal lengths of RTP packets. 146 | value += (now - start) - duration; 147 | //Log.d(TAG, "sum1: "+duration/1000000+" sum2: "+(now-start)/1000000+" drift: "+((now-start)-duration)/1000000+" v: "+value/1000000); 148 | } 149 | if (c<5) { 150 | // We ignore the first 20 measured values because they may not be accurate 151 | c++; 152 | m = value; 153 | } else { 154 | m = (m*q+value)/(q+1); 155 | if (q2000) { 115 | delta2 = 0; 116 | if (sps != null) { 117 | buffer = socket.requestBuffer(); 118 | socket.markNextPacket(); 119 | socket.updateTimestamp(ts); 120 | System.arraycopy(sps, 0, buffer, rtphl, sps.length); 121 | super.send(rtphl+sps.length); 122 | } 123 | if (pps != null) { 124 | buffer = socket.requestBuffer(); 125 | socket.updateTimestamp(ts); 126 | socket.markNextPacket(); 127 | System.arraycopy(pps, 0, buffer, rtphl, pps.length); 128 | super.send(rtphl+pps.length); 129 | } 130 | } 131 | 132 | stats.push(duration); 133 | // Computes the average duration of a NAL unit 134 | delay = stats.average(); 135 | //Log.d(TAG,"duration: "+duration/1000000+" delay: "+delay/1000000); 136 | 137 | } 138 | } catch (IOException e) { 139 | } catch (InterruptedException e) {} 140 | 141 | Log.d(TAG,"H264 packetizer stopped !"); 142 | 143 | } 144 | 145 | /** 146 | * Reads a NAL unit in the FIFO and sends it. 147 | * If it is too big, we split it in FU-A units (RFC 3984). 148 | */ 149 | @SuppressLint("NewApi") 150 | private void send() throws IOException, InterruptedException { 151 | int sum = 1, len = 0, type; 152 | 153 | if (streamType == 0) { 154 | // NAL units are preceeded by their length, we parse the length 155 | fill(header,0,5); 156 | ts += delay; 157 | naluLength = header[3]&0xFF | (header[2]&0xFF)<<8 | (header[1]&0xFF)<<16 | (header[0]&0xFF)<<24; 158 | 159 | if (naluLength>100000 || naluLength<0) resync(); 160 | } else if (streamType == 1) { 161 | // NAL units are preceeded with 0x00000001 162 | fill(header,0,5); 163 | ts = ((ScreenInputStream)is). getLastts(); 164 | //ts += delay; 165 | naluLength = is.available(); 166 | Log.d(TAG,"header is "+header[0]+" "+header[1]+" "+header[2]+" "+header[3]+" "+header[4]+" ts = "+ts+" nalu len = "+naluLength+""); 167 | if (!(header[0]==0 && header[1]==0 && header[2]==0)) { 168 | // Turns out, the NAL units are not preceeded with 0x00000001 169 | Log.e(TAG, "NAL units are not preceeded by 0x00000001"); 170 | streamType = 2; 171 | return; 172 | } 173 | } else { 174 | // Nothing preceededs the NAL units 175 | fill(header,0,1); 176 | header[4] = header[0]; 177 | ts = ((ScreenInputStream)is). getLastts(); 178 | //ts += delay; 179 | naluLength = is.available(); 180 | } 181 | 182 | // Parses the NAL unit type 183 | type = header[4]&0x1F; 184 | 185 | Log.d(TAG,"NAL type is "+type+""); 186 | 187 | // The stream already contains NAL unit type 7 or 8, we don't need 188 | // to add them to the stream ourselves 189 | if (type == 7 || type == 8) { 190 | Log.v(TAG,"SPS or PPS present in the stream."); 191 | count++; 192 | if (count>4) { 193 | sps = null; 194 | pps = null; 195 | } 196 | } 197 | 198 | //Log.d(TAG,"- Nal unit length: " + naluLength + " delay: "+delay/1000000+" type: "+type); 199 | 200 | // Small NAL unit => Single NAL unit 201 | if (naluLength<=MAXPACKETSIZE-rtphl-2) { 202 | buffer = socket.requestBuffer(); 203 | buffer[rtphl] = header[4]; 204 | len = fill(buffer, rtphl+1, naluLength-1); 205 | socket.updateTimestamp(ts); 206 | socket.markNextPacket(); 207 | super.send(naluLength+rtphl); 208 | //Log.d(TAG,"----- Single NAL unit - len:"+len+" delay: "+delay); 209 | } 210 | // Large NAL unit => Split nal unit 211 | else { 212 | 213 | // Set FU-A header 214 | header[1] = (byte) (header[4] & 0x1F); // FU header type 215 | header[1] += 0x80; // Start bit 216 | // Set FU-A indicator 217 | header[0] = (byte) ((header[4] & 0x60) & 0xFF); // FU indicator NRI 218 | header[0] += 28; 219 | 220 | while (sum < naluLength) { 221 | buffer = socket.requestBuffer(); 222 | buffer[rtphl] = header[0]; 223 | buffer[rtphl+1] = header[1]; 224 | socket.updateTimestamp(ts); 225 | if ((len = fill(buffer, rtphl+2, naluLength-sum > MAXPACKETSIZE-rtphl-2 ? MAXPACKETSIZE-rtphl-2 : naluLength-sum ))<0) return; sum += len; 226 | // Last packet before next NAL 227 | if (sum >= naluLength) { 228 | // End bit on 229 | buffer[rtphl+1] += 0x40; 230 | socket.markNextPacket(); 231 | } 232 | super.send(len+rtphl+2); 233 | // Switch start bit 234 | header[1] = (byte) (header[1] & 0x7F); 235 | //Log.d(TAG,"----- FU-A unit, sum:"+sum); 236 | } 237 | } 238 | } 239 | 240 | private int fill(byte[] buffer, int offset,int length) throws IOException { 241 | int sum = 0, len; 242 | while (sum0 && naluLength<100000) { 270 | oldtime = System.nanoTime(); 271 | Log.e(TAG,"A NAL unit may have been found in the bit stream !"); 272 | break; 273 | } 274 | if (naluLength==0) { 275 | Log.e(TAG,"NAL unit with NULL size found..."); 276 | } else if (header[3]==0xFF && header[2]==0xFF && header[1]==0xFF && header[0]==0xFF) { 277 | Log.e(TAG,"NAL unit with 0xFFFFFFFF size found..."); 278 | } 279 | } 280 | 281 | } 282 | 283 | } 284 | 285 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/MediaStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com 3 | * 4 | * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) 5 | * 6 | * Spydroid is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This source code is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this source code; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | package com.zpf.androidshow.rtsp; 22 | 23 | import android.annotation.SuppressLint; 24 | import android.media.MediaCodec; 25 | import android.media.MediaRecorder; 26 | import android.net.LocalServerSocket; 27 | import android.net.LocalSocket; 28 | import android.net.LocalSocketAddress; 29 | import android.util.Log; 30 | 31 | import java.io.IOException; 32 | import java.net.InetAddress; 33 | import java.util.Random; 34 | 35 | /** 36 | * A MediaRecorder that streams what it records using a packetizer from the rtp package. 37 | * You can't use this class directly ! 38 | */ 39 | public abstract class MediaStream implements Stream { 40 | 41 | protected static final String TAG = "MediaStream"; 42 | 43 | /** Raw audio/video will be encoded using the MediaRecorder API. */ 44 | public static final byte MODE_MEDIARECORDER_API = 0x01; 45 | 46 | /** Raw audio/video will be encoded using the MediaCodec API with buffers. */ 47 | public static final byte MODE_MEDIACODEC_API = 0x02; 48 | 49 | /** Raw audio/video will be encoded using the MediaCode API with a surface. */ 50 | public static final byte MODE_MEDIACODEC_API_2 = 0x05; 51 | 52 | /** Prefix that will be used for all shared preferences saved by libstreaming */ 53 | protected static final String PREF_PREFIX = "libstreaming-"; 54 | 55 | /** The packetizer that will read the output of the camera and send RTP packets over the networkd. */ 56 | protected AbstractPacketizer mPacketizer = null; 57 | 58 | protected static byte sSuggestedMode = MODE_MEDIARECORDER_API; 59 | protected byte mMode, mRequestedMode; 60 | 61 | protected boolean mStreaming = false, mConfigured = false; 62 | protected int mRtpPort = 0, mRtcpPort = 0; 63 | protected InetAddress mDestination; 64 | protected LocalSocket mReceiver, mSender = null; 65 | private LocalServerSocket mLss = null; 66 | private int mSocketId, mTTL = 64; 67 | 68 | protected MediaRecorder mMediaRecorder; 69 | protected MediaCodec mMediaCodec; 70 | 71 | static { 72 | // We determine wether or not the MediaCodec API should be used 73 | try { 74 | Class.forName("android.media.MediaCodec"); 75 | // Will be set to MODE_MEDIACODEC_API at some point... 76 | sSuggestedMode = MODE_MEDIACODEC_API; 77 | Log.i(TAG,"Phone supports the MediaCoded API"); 78 | } catch (ClassNotFoundException e) { 79 | sSuggestedMode = MODE_MEDIARECORDER_API; 80 | Log.i(TAG,"Phone does not support the MediaCodec API"); 81 | } 82 | } 83 | 84 | public MediaStream() { 85 | mRequestedMode = sSuggestedMode; 86 | mMode = sSuggestedMode; 87 | } 88 | 89 | /** 90 | * Sets the destination ip address of the stream. 91 | * @param dest The destination address of the stream 92 | */ 93 | public void setDestinationAddress(InetAddress dest) { 94 | mDestination = dest; 95 | } 96 | 97 | /** 98 | * Sets the destination ports of the stream. 99 | * If an odd number is supplied for the destination port then the next 100 | * lower even number will be used for RTP and it will be used for RTCP. 101 | * If an even number is supplied, it will be used for RTP and the next odd 102 | * number will be used for RTCP. 103 | * @param dport The destination port 104 | */ 105 | public void setDestinationPorts(int dport) { 106 | if (dport % 2 == 1) { 107 | mRtpPort = dport-1; 108 | mRtcpPort = dport; 109 | } else { 110 | mRtpPort = dport; 111 | mRtcpPort = dport+1; 112 | } 113 | } 114 | 115 | /** 116 | * Sets the destination ports of the stream. 117 | * @param rtpPort Destination port that will be used for RTP 118 | * @param rtcpPort Destination port that will be used for RTCP 119 | */ 120 | public void setDestinationPorts(int rtpPort, int rtcpPort) { 121 | mRtpPort = rtpPort; 122 | mRtcpPort = rtcpPort; 123 | } 124 | 125 | /** 126 | * Sets the Time To Live of packets sent over the network. 127 | * @param ttl The time to live 128 | * @throws IOException 129 | */ 130 | public void setTimeToLive(int ttl) throws IOException { 131 | mTTL = ttl; 132 | } 133 | 134 | /** 135 | * Returns a pair of destination ports, the first one is the 136 | * one used for RTP and the second one is used for RTCP. 137 | **/ 138 | public int[] getDestinationPorts() { 139 | return new int[] { 140 | mRtpPort, 141 | mRtcpPort 142 | }; 143 | } 144 | 145 | /** 146 | * Returns a pair of source ports, the first one is the 147 | * one used for RTP and the second one is used for RTCP. 148 | **/ 149 | public int[] getLocalPorts() { 150 | return new int[] { 151 | this.mPacketizer.getRtpSocket().getLocalPort(), 152 | this.mPacketizer.getRtcpSocket().getLocalPort() 153 | }; 154 | } 155 | 156 | protected abstract void encodeWithMediaRecorder() throws IOException; 157 | 158 | protected abstract void encodeWithMediaCodec() throws IOException; 159 | 160 | /** 161 | * Sets the streaming method that will be used. 162 | * 163 | * If the mode is set to {@link #MODE_MEDIARECORDER_API}, raw audio/video will be encoded 164 | * using the MediaRecorder API.
165 | * 166 | * If the mode is set to {@link #MODE_MEDIACODEC_API} or to {@link #MODE_MEDIACODEC_API_2}, 167 | * audio/video will be encoded with using the MediaCodec.
168 | * 169 | * The {@link #MODE_MEDIACODEC_API_2} mode only concerns {@link VideoStream}, it makes 170 | * use of the createInputSurface() method of the MediaCodec API (Android 4.3 is needed there).
171 | * 172 | * @param mode Can be {@link #MODE_MEDIARECORDER_API}, {@link #MODE_MEDIACODEC_API} or {@link #MODE_MEDIACODEC_API_2} 173 | */ 174 | public void setStreamingMethod(byte mode) { 175 | mRequestedMode = mode; 176 | } 177 | 178 | /** 179 | * Returns the packetizer associated with the {@link MediaStream}. 180 | * @return The packetizer 181 | */ 182 | public AbstractPacketizer getPacketizer() { 183 | return mPacketizer; 184 | } 185 | 186 | /** 187 | * Returns an approximation of the bit rate consumed by the stream in bit per seconde. 188 | */ 189 | public long getBitrate() { 190 | return !mStreaming ? 0 : mPacketizer.getRtpSocket().getBitrate(); 191 | } 192 | 193 | /** 194 | * Indicates if the {@link MediaStream} is streaming. 195 | * @return A boolean indicating if the {@link MediaStream} is streaming 196 | */ 197 | public boolean isStreaming() { 198 | return mStreaming; 199 | } 200 | 201 | /** 202 | * Configures the stream with the settings supplied with 203 | */ 204 | public synchronized void configure() throws IllegalStateException, IOException { 205 | if (mStreaming) throw new IllegalStateException("Can't be called while streaming."); 206 | mMode = mRequestedMode; 207 | mConfigured = true; 208 | } 209 | 210 | /** Starts the stream. */ 211 | public synchronized void start() throws IllegalStateException, IOException { 212 | encodeWithMediaCodec(); 213 | } 214 | 215 | /** Stops the stream. */ 216 | @SuppressLint("NewApi") 217 | public synchronized void stop() { 218 | if (mStreaming) { 219 | mStreaming = false; 220 | } 221 | } 222 | 223 | /** 224 | * Returns a description of the stream using SDP. 225 | * This method can only be called after {@link Stream#configure()}. 226 | * @throws IllegalStateException Thrown when {@link Stream#configure()} wa not called. 227 | */ 228 | public abstract String getSessionDescription(); 229 | 230 | /** 231 | * Returns the SSRC of the underlying 232 | * @return the SSRC of the stream 233 | */ 234 | public int getSSRC() { 235 | return getPacketizer().getSSRC(); 236 | } 237 | 238 | protected void createSockets() throws IOException { 239 | 240 | final String LOCAL_ADDR = "net.majorkernelpanic.streaming-"; 241 | 242 | for (int i=0;i<10;i++) { 243 | try { 244 | mSocketId = new Random().nextInt(); 245 | mLss = new LocalServerSocket(LOCAL_ADDR+mSocketId); 246 | break; 247 | } catch (IOException e1) {} 248 | } 249 | 250 | mReceiver = new LocalSocket(); 251 | mReceiver.connect( new LocalSocketAddress(LOCAL_ADDR+mSocketId)); 252 | mReceiver.setReceiveBufferSize(500000); 253 | mReceiver.setSoTimeout(3000); 254 | mSender = mLss.accept(); 255 | mSender.setSendBufferSize(500000); 256 | } 257 | 258 | protected void closeSockets() { 259 | try { 260 | mReceiver.close(); 261 | } catch (Exception e) { 262 | e.printStackTrace(); 263 | } 264 | try { 265 | mSender.close(); 266 | } catch (Exception e) { 267 | e.printStackTrace(); 268 | } 269 | try { 270 | mLss.close(); 271 | } catch (Exception e) { 272 | e.printStackTrace(); 273 | } 274 | mLss = null; 275 | mSender = null; 276 | mReceiver = null; 277 | } 278 | 279 | } 280 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/RtpSocket.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com 3 | * 4 | * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) 5 | * 6 | * Spydroid is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This source code is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this source code; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | package com.zpf.androidshow.rtsp; 22 | 23 | import android.os.SystemClock; 24 | import android.util.Log; 25 | 26 | import java.io.IOException; 27 | import java.net.DatagramPacket; 28 | import java.net.InetAddress; 29 | import java.net.MulticastSocket; 30 | import java.util.concurrent.Semaphore; 31 | import java.util.concurrent.TimeUnit; 32 | 33 | /** 34 | * A basic implementation of an RTP socket. 35 | * It implements a buffering mechanism, relying on a FIFO of buffers and a Thread. 36 | * That way, if a packetizer tries to send many packets too quickly, the FIFO will 37 | * grow and packets will be sent one by one smoothly. 38 | */ 39 | public class RtpSocket implements Runnable { 40 | 41 | public static final String TAG = "RtpSocket"; 42 | 43 | public static final int RTP_HEADER_LENGTH = 12; 44 | public static final int MTU = 1300; 45 | 46 | private MulticastSocket mSocket; 47 | private DatagramPacket[] mPackets; 48 | private byte[][] mBuffers; 49 | private long[] mTimestamps; 50 | 51 | private SenderReport mReport; 52 | 53 | private Semaphore mBufferRequested, mBufferCommitted; 54 | private Thread mThread; 55 | 56 | private long mCacheSize; 57 | private long mClock = 0; 58 | private long mOldTimestamp = 0; 59 | private int mSsrc, mSeq = 0, mPort = -1; 60 | private int mBufferCount, mBufferIn, mBufferOut; 61 | private int mCount = 0; 62 | 63 | private AverageBitrate mAverageBitrate; 64 | 65 | /** 66 | * This RTP socket implements a buffering mechanism relying on a FIFO of buffers and a Thread. 67 | * @throws IOException 68 | */ 69 | public RtpSocket() { 70 | 71 | mCacheSize = 00; 72 | mBufferCount = 300; // TODO: reajust that when the FIFO is full 73 | mBuffers = new byte[mBufferCount][]; 74 | mPackets = new DatagramPacket[mBufferCount]; 75 | mReport = new SenderReport(); 76 | mAverageBitrate = new AverageBitrate(); 77 | 78 | resetFifo(); 79 | 80 | for (int i=0; i Source Identifier(0) */ 91 | /* | || | */ 92 | mBuffers[i][0] = (byte) Integer.parseInt("10000000",2); 93 | 94 | /* Payload Type */ 95 | mBuffers[i][1] = (byte) 96; 96 | 97 | /* Byte 2,3 -> Sequence Number */ 98 | /* Byte 4,5,6,7 -> Timestamp */ 99 | /* Byte 8,9,10,11 -> Sync Source Identifier */ 100 | 101 | } 102 | 103 | try { 104 | mSocket = new MulticastSocket(); 105 | } catch (Exception e) { 106 | throw new RuntimeException(e.getMessage()); 107 | } 108 | 109 | } 110 | 111 | private void resetFifo() { 112 | mCount = 0; 113 | mBufferIn = 0; 114 | mBufferOut = 0; 115 | mTimestamps = new long[mBufferCount]; 116 | mBufferRequested = new Semaphore(mBufferCount); 117 | mBufferCommitted = new Semaphore(0); 118 | mReport.reset(); 119 | mAverageBitrate.reset(); 120 | } 121 | 122 | /** Closes the underlying socket. */ 123 | public void close() { 124 | mSocket.close(); 125 | } 126 | 127 | /** Sets the SSRC of the stream. */ 128 | public void setSSRC(int ssrc) { 129 | this.mSsrc = ssrc; 130 | for (int i=0;i=mBufferCount) mBufferIn = 0; 198 | mBufferCommitted.release(); 199 | 200 | } 201 | 202 | /** Sends the RTP packet over the network. */ 203 | public void commitBuffer(int length) throws IOException { 204 | updateSequence(); 205 | mPackets[mBufferIn].setLength(length); 206 | 207 | mAverageBitrate.push(length); 208 | 209 | if (++mBufferIn>=mBufferCount) mBufferIn = 0; 210 | mBufferCommitted.release(); 211 | 212 | if (mThread == null) { 213 | mThread = new Thread(this); 214 | mThread.start(); 215 | } 216 | 217 | } 218 | 219 | /** Returns an approximation of the bitrate of the RTP stream in bit per seconde. */ 220 | public long getBitrate() { 221 | return mAverageBitrate.average(); 222 | } 223 | 224 | /** Increments the sequence number. */ 225 | private void updateSequence() { 226 | setLong(mBuffers[mBufferIn], ++mSeq, 2, 4); 227 | } 228 | 229 | /** 230 | * Overwrites the timestamp in the packet. 231 | * @param timestamp The new timestamp in ns. 232 | **/ 233 | public void updateTimestamp(long timestamp) { 234 | mTimestamps[mBufferIn] = timestamp; 235 | setLong(mBuffers[mBufferIn], (timestamp/100L)*(mClock/1000L)/10000L, 4, 8); 236 | } 237 | 238 | /** Sets the marker in the RTP packet. */ 239 | public void markNextPacket() { 240 | mBuffers[mBufferIn][1] |= 0x80; 241 | } 242 | 243 | /** The Thread sends the packets in the FIFO one by one at a constant rate. */ 244 | @Override 245 | public void run() { 246 | Statistics stats = new Statistics(50,3000); 247 | try { 248 | // Caches mCacheSize milliseconds of the stream in the FIFO. 249 | Thread.sleep(mCacheSize); 250 | long delta = 0; 251 | while (mBufferCommitted.tryAcquire(4,TimeUnit.SECONDS)) { 252 | if (mOldTimestamp != 0) { 253 | // We use our knowledge of the clock rate of the stream and the difference between two timestamps to 254 | // compute the time lapse that the packet represents. 255 | if ((mTimestamps[mBufferOut]-mOldTimestamp)>0) { 256 | stats.push(mTimestamps[mBufferOut]-mOldTimestamp); 257 | long d = stats.average()/1000000; 258 | //Log.d(TAG,"delay: "+d+" d: "+(mTimestamps[mBufferOut]-mOldTimestamp)/1000000); 259 | // We ensure that packets are sent at a constant and suitable rate no matter how the RtpSocket is used. 260 | if (mCacheSize>0) Thread.sleep(d); 261 | } else if ((mTimestamps[mBufferOut]-mOldTimestamp)<0) { 262 | Log.e(TAG, "TS: "+mTimestamps[mBufferOut]+" OLD: "+mOldTimestamp); 263 | } 264 | delta += mTimestamps[mBufferOut]-mOldTimestamp; 265 | if (delta>500000000 || delta<0) { 266 | //Log.d(TAG,"permits: "+mBufferCommitted.availablePermits()); 267 | delta = 0; 268 | } 269 | } 270 | mReport.update(mPackets[mBufferOut].getLength(), System.nanoTime(),(mTimestamps[mBufferOut]/100L)*(mClock/1000L)/10000L); 271 | mOldTimestamp = mTimestamps[mBufferOut]; 272 | if (mCount++>30) mSocket.send(mPackets[mBufferOut]); 273 | if (++mBufferOut>=mBufferCount) mBufferOut = 0; 274 | mBufferRequested.release(); 275 | } 276 | } catch (Exception e) { 277 | e.printStackTrace(); 278 | } 279 | mThread = null; 280 | resetFifo(); 281 | } 282 | 283 | private void setLong(byte[] buffer, long n, int begin, int end) { 284 | for (end--; end >= begin; end--) { 285 | buffer[end] = (byte) (n % 256); 286 | n >>= 8; 287 | } 288 | } 289 | 290 | /** 291 | * Computes an average bit rate. 292 | **/ 293 | protected static class AverageBitrate { 294 | 295 | private final static long RESOLUTION = 200; 296 | 297 | private long mOldNow, mNow, mDelta; 298 | private long[] mElapsed, mSum; 299 | private int mCount, mIndex, mTotal; 300 | private int mSize; 301 | 302 | public AverageBitrate() { 303 | mSize = 5000/((int)RESOLUTION); 304 | reset(); 305 | } 306 | 307 | public AverageBitrate(int delay) { 308 | mSize = delay/((int)RESOLUTION); 309 | reset(); 310 | } 311 | 312 | public void reset() { 313 | mSum = new long[mSize]; 314 | mElapsed = new long[mSize]; 315 | mNow = SystemClock.elapsedRealtime(); 316 | mOldNow = mNow; 317 | mCount = 0; 318 | mDelta = 0; 319 | mTotal = 0; 320 | mIndex = 0; 321 | } 322 | 323 | public void push(int length) { 324 | mNow = SystemClock.elapsedRealtime(); 325 | if (mCount>0) { 326 | mDelta += mNow - mOldNow; 327 | mTotal += length; 328 | if (mDelta>RESOLUTION) { 329 | mSum[mIndex] = mTotal; 330 | mTotal = 0; 331 | mElapsed[mIndex] = mDelta; 332 | mDelta = 0; 333 | mIndex++; 334 | if (mIndex>=mSize) mIndex = 0; 335 | } 336 | } 337 | mOldNow = mNow; 338 | mCount++; 339 | } 340 | 341 | public int average() { 342 | long delta = 0, sum = 0; 343 | for (int i=0;i0?8000*sum/delta:0); 349 | } 350 | 351 | } 352 | 353 | /** Computes the proper rate at which packets are sent. */ 354 | protected static class Statistics { 355 | 356 | public final static String TAG = "Statistics"; 357 | 358 | private int count=500, c = 0; 359 | private float m = 0, q = 0; 360 | private long elapsed = 0; 361 | private long start = 0; 362 | private long duration = 0; 363 | private long period = 6000000000L; 364 | private boolean initoffset = false; 365 | 366 | public Statistics(int count, long period) { 367 | this.count = count; 368 | this.period = period*1000000L; 369 | } 370 | 371 | public void push(long value) { 372 | duration += value; 373 | elapsed += value; 374 | if (elapsed>period) { 375 | elapsed = 0; 376 | long now = System.nanoTime(); 377 | if (!initoffset || (now - start < 0)) { 378 | start = now; 379 | duration = 0; 380 | initoffset = true; 381 | } 382 | value -= (now - start) - duration; 383 | //Log.d(TAG, "sum1: "+duration/1000000+" sum2: "+(now-start)/1000000+" drift: "+((now-start)-duration)/1000000+" v: "+value/1000000); 384 | } 385 | if (c<40) { 386 | // We ignore the first 40 measured values because they may not be accurate 387 | c++; 388 | m = value; 389 | } else { 390 | m = (m*q+value)/(q+1); 391 | if (q0 ? l : 0; 398 | } 399 | 400 | } 401 | 402 | } 403 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/RtspServer.java: -------------------------------------------------------------------------------- 1 | package com.zpf.androidshow.rtsp; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.content.SharedPreferences; 6 | import android.os.Binder; 7 | import android.os.IBinder; 8 | import android.preference.PreferenceManager; 9 | import android.support.annotation.Nullable; 10 | import android.util.Log; 11 | 12 | import java.io.BufferedReader; 13 | import java.io.IOException; 14 | import java.io.InputStreamReader; 15 | import java.io.OutputStream; 16 | import java.net.BindException; 17 | import java.net.InetAddress; 18 | import java.net.ServerSocket; 19 | import java.net.Socket; 20 | import java.net.SocketException; 21 | import java.util.HashMap; 22 | import java.util.LinkedList; 23 | import java.util.Locale; 24 | import java.util.WeakHashMap; 25 | import java.util.regex.Matcher; 26 | import java.util.regex.Pattern; 27 | 28 | /** 29 | * Implementation of a subset of the RTSP protocol (RFC 2326). 30 | * 31 | * It allows remote control of an android device cameras & microphone. 32 | * For each connected client, a Session is instantiated. 33 | * The Session will start or stop streams according to what the client wants. 34 | * 35 | */ 36 | public class RtspServer extends Service { 37 | 38 | public final static String TAG = "RtspServer"; 39 | 40 | /** The server name that will appear in responses. */ 41 | public static String SERVER_NAME = "MajorKernelPanic RTSP Server"; 42 | 43 | /** Port used by default. */ 44 | public static final int DEFAULT_RTSP_PORT = 8086; 45 | 46 | /** Port already in use. */ 47 | public final static int ERROR_BIND_FAILED = 0x00; 48 | 49 | /** A stream could not be started. */ 50 | public final static int ERROR_START_FAILED = 0x01; 51 | 52 | /** Streaming started. */ 53 | public final static int MESSAGE_STREAMING_STARTED = 0X00; 54 | 55 | /** Streaming stopped. */ 56 | public final static int MESSAGE_STREAMING_STOPPED = 0X01; 57 | 58 | /** Key used in the SharedPreferences to store whether the RTSP server is enabled or not. */ 59 | public final static String KEY_ENABLED = "rtsp_enabled"; 60 | 61 | /** Key used in the SharedPreferences for the port used by the RTSP server. */ 62 | public final static String KEY_PORT = "rtsp_port"; 63 | 64 | protected SessionBuilder mSessionBuilder; 65 | protected SharedPreferences mSharedPreferences; 66 | protected boolean mEnabled = true; 67 | protected int mPort = DEFAULT_RTSP_PORT; 68 | protected WeakHashMap mSessions = new WeakHashMap(2); 69 | 70 | private RequestListener mListenerThread; 71 | private final IBinder mBinder = new LocalBinder(); 72 | private boolean mRestart = false; 73 | private final LinkedList mListeners = new LinkedList(); 74 | 75 | 76 | public RtspServer() { 77 | } 78 | 79 | /** Be careful: those callbacks won't necessarily be called from the ui thread ! */ 80 | public interface CallbackListener { 81 | 82 | /** Called when an error occurs. */ 83 | void onError(RtspServer server, Exception e, int error); 84 | 85 | /** Called when streaming starts/stops. */ 86 | void onMessage(RtspServer server, int message); 87 | 88 | } 89 | 90 | /** 91 | * See {@link RtspServer.CallbackListener} to check out what events will be fired once you set up a listener. 92 | * @param listener The listener 93 | */ 94 | public void addCallbackListener(CallbackListener listener) { 95 | synchronized (mListeners) { 96 | if (mListeners.size() > 0) { 97 | for (CallbackListener cl : mListeners) { 98 | if (cl == listener) return; 99 | } 100 | } 101 | mListeners.add(listener); 102 | } 103 | } 104 | 105 | /** 106 | * Removes the listener. 107 | * @param listener The listener 108 | */ 109 | public void removeCallbackListener(CallbackListener listener) { 110 | synchronized (mListeners) { 111 | mListeners.remove(listener); 112 | } 113 | } 114 | 115 | /** Returns the port used by the RTSP server. */ 116 | public int getPort() { 117 | return mPort; 118 | } 119 | 120 | /** 121 | * Sets the port for the RTSP server to use. 122 | * @param port The port 123 | */ 124 | public void setPort(int port) { 125 | SharedPreferences.Editor editor = mSharedPreferences.edit(); 126 | editor.putString(KEY_PORT, String.valueOf(port)); 127 | editor.commit(); 128 | } 129 | 130 | /** 131 | * Starts (or restart if needed, if for example the configuration 132 | * of the server has been modified) the RTSP server. 133 | */ 134 | public void start() { 135 | if (!mEnabled || mRestart) stop(); 136 | if (mEnabled && mListenerThread == null) { 137 | try { 138 | mListenerThread = new RequestListener(); 139 | } catch (Exception e) { 140 | mListenerThread = null; 141 | } 142 | } 143 | mRestart = false; 144 | } 145 | 146 | /** 147 | * Stops the RTSP server but not the Android Service. 148 | * To stop the Android Service you need to call {@link android.content.Context#stopService(Intent)}; 149 | */ 150 | public void stop() { 151 | if (mListenerThread != null) { 152 | try { 153 | mListenerThread.kill(); 154 | for ( Session session : mSessions.keySet() ) { 155 | if ( session != null ) { 156 | if (session.isStreaming()) session.stop(); 157 | } 158 | } 159 | } catch (Exception e) { 160 | } finally { 161 | mListenerThread = null; 162 | } 163 | } 164 | } 165 | 166 | /** Returns whether or not the RTSP server is streaming to some client(s). */ 167 | public boolean isStreaming() { 168 | for ( Session session : mSessions.keySet() ) { 169 | if ( session != null ) { 170 | if (session.isStreaming()) return true; 171 | } 172 | } 173 | return false; 174 | } 175 | 176 | public boolean isEnabled() { 177 | return mEnabled; 178 | } 179 | 180 | /** Returns the bandwidth consumed by the RTSP server in bits per second. */ 181 | public long getBitrate() { 182 | long bitrate = 0; 183 | for ( Session session : mSessions.keySet() ) { 184 | if ( session != null ) { 185 | if (session.isStreaming()) bitrate += session.getBitrate(); 186 | } 187 | } 188 | return bitrate; 189 | } 190 | 191 | @Override 192 | public int onStartCommand(Intent intent, int flags, int startId) { 193 | return START_STICKY; 194 | } 195 | 196 | @Override 197 | public void onCreate() { 198 | 199 | // Let's restore the state of the service 200 | mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 201 | mPort = Integer.parseInt(mSharedPreferences.getString(KEY_PORT, String.valueOf(mPort))); 202 | mEnabled = mSharedPreferences.getBoolean(KEY_ENABLED, mEnabled); 203 | 204 | // If the configuration is modified, the server will adjust 205 | mSharedPreferences.registerOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener); 206 | 207 | start(); 208 | } 209 | 210 | @Override 211 | public void onDestroy() { 212 | stop(); 213 | mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener); 214 | } 215 | 216 | private SharedPreferences.OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { 217 | @Override 218 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 219 | 220 | if (key.equals(KEY_PORT)) { 221 | int port = Integer.parseInt(sharedPreferences.getString(KEY_PORT, String.valueOf(mPort))); 222 | if (port != mPort) { 223 | mPort = port; 224 | mRestart = true; 225 | start(); 226 | } 227 | } 228 | else if (key.equals(KEY_ENABLED)) { 229 | mEnabled = sharedPreferences.getBoolean(KEY_ENABLED, mEnabled); 230 | start(); 231 | } 232 | } 233 | }; 234 | 235 | /** The Binder you obtain when a connection with the Service is established. */ 236 | public class LocalBinder extends Binder { 237 | public RtspServer getService() { 238 | return RtspServer.this; 239 | } 240 | } 241 | 242 | @Override 243 | public IBinder onBind(Intent intent) { 244 | return mBinder; 245 | } 246 | 247 | protected void postMessage(int id) { 248 | synchronized (mListeners) { 249 | if (mListeners.size() > 0) { 250 | for (CallbackListener cl : mListeners) { 251 | cl.onMessage(this, id); 252 | } 253 | } 254 | } 255 | } 256 | 257 | protected void postError(Exception exception, int id) { 258 | synchronized (mListeners) { 259 | if (mListeners.size() > 0) { 260 | for (CallbackListener cl : mListeners) { 261 | cl.onError(this, exception, id); 262 | } 263 | } 264 | } 265 | } 266 | 267 | /** 268 | * By default the RTSP uses {@link UriParser} to parse the URI requested by the client 269 | * but you can change that behavior by override this method. 270 | * @param uri The uri that the client has requested 271 | * @param client The socket associated to the client 272 | * @return A proper session 273 | */ 274 | protected Session handleRequest(String uri, Socket client) throws IllegalStateException, IOException { 275 | Session session = UriParser.parse(uri); 276 | session.setOrigin(client.getLocalAddress().getHostAddress()); 277 | if (session.getDestination()==null) { 278 | session.setDestination(client.getInetAddress().getHostAddress()); 279 | } 280 | return session; 281 | } 282 | 283 | class RequestListener extends Thread implements Runnable { 284 | 285 | private final ServerSocket mServer; 286 | 287 | public RequestListener() throws IOException { 288 | try { 289 | mServer = new ServerSocket(mPort); 290 | start(); 291 | } catch (BindException e) { 292 | Log.e(TAG,"Port already in use !"); 293 | postError(e, ERROR_BIND_FAILED); 294 | throw e; 295 | } 296 | } 297 | 298 | public void run() { 299 | Log.i(TAG,"RTSP server listening on port "+mServer.getLocalPort()); 300 | while (!Thread.interrupted()) { 301 | try { 302 | new WorkerThread(mServer.accept()).start(); 303 | } catch (SocketException e) { 304 | break; 305 | } catch (IOException e) { 306 | Log.e(TAG,e.getMessage()); 307 | continue; 308 | } 309 | } 310 | Log.i(TAG,"RTSP server stopped !"); 311 | } 312 | 313 | public void kill() { 314 | try { 315 | mServer.close(); 316 | } catch (IOException e) {} 317 | try { 318 | this.join(); 319 | } catch (InterruptedException ignore) {} 320 | } 321 | 322 | } 323 | 324 | // One thread per client 325 | class WorkerThread extends Thread implements Runnable { 326 | 327 | private final Socket mClient; 328 | private final OutputStream mOutput; 329 | private final BufferedReader mInput; 330 | 331 | // Each client has an associated session 332 | private Session mSession; 333 | 334 | public WorkerThread(final Socket client) throws IOException { 335 | mInput = new BufferedReader(new InputStreamReader(client.getInputStream())); 336 | mOutput = client.getOutputStream(); 337 | mClient = client; 338 | mSession = new Session(); 339 | } 340 | 341 | public void run() { 342 | Request request; 343 | Response response; 344 | 345 | Log.i(TAG, "Connection from "+mClient.getInetAddress().getHostAddress()); 346 | 347 | while (!Thread.interrupted()) { 348 | 349 | request = null; 350 | response = null; 351 | 352 | // Parse the request 353 | try { 354 | request = Request.parseRequest(mInput); 355 | } catch (SocketException e) { 356 | // Client has left 357 | break; 358 | } catch (Exception e) { 359 | // We don't understand the request :/ 360 | response = new Response(); 361 | response.status = Response.STATUS_BAD_REQUEST; 362 | } 363 | 364 | // Do something accordingly like starting the streams, sending a session description 365 | if (request != null) { 366 | try { 367 | response = processRequest(request); 368 | } 369 | catch (Exception e) { 370 | // This alerts the main thread that something has gone wrong in this thread 371 | postError(e, ERROR_START_FAILED); 372 | Log.e(TAG,e.getMessage()!=null?e.getMessage():"An error occurred"); 373 | e.printStackTrace(); 374 | response = new Response(request); 375 | } 376 | } 377 | 378 | // We always send a response 379 | // The client will receive an "INTERNAL SERVER ERROR" if an exception has been thrown at some point 380 | try { 381 | response.send(mOutput); 382 | } catch (IOException e) { 383 | Log.e(TAG,"Response was not sent properly"); 384 | break; 385 | } 386 | 387 | } 388 | 389 | // Streaming stops when client disconnects 390 | boolean streaming = isStreaming(); 391 | mSession.syncStop(); 392 | if (streaming && !isStreaming()) { 393 | postMessage(MESSAGE_STREAMING_STOPPED); 394 | } 395 | mSession.release(); 396 | 397 | try { 398 | mClient.close(); 399 | } catch (IOException ignore) {} 400 | 401 | Log.i(TAG, "Client disconnected"); 402 | 403 | } 404 | 405 | public Response processRequest(Request request) throws IllegalStateException, IOException { 406 | Response response = new Response(request); 407 | 408 | /* ********************************************************************************** */ 409 | /* ********************************* Method DESCRIBE ******************************** */ 410 | /* ********************************************************************************** */ 411 | if (request.method.equalsIgnoreCase("DESCRIBE")) { 412 | 413 | // Parse the requested URI and configure the session 414 | mSession = handleRequest(request.uri, mClient); 415 | mSessions.put(mSession, null); 416 | mSession.syncConfigure(); 417 | 418 | String requestContent = mSession.getSessionDescription(); 419 | String requestAttributes = 420 | "Content-Base: "+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/\r\n" + 421 | "Content-Type: application/sdp\r\n"; 422 | 423 | response.attributes = requestAttributes; 424 | response.content = requestContent; 425 | 426 | // If no exception has been thrown, we reply with OK 427 | response.status = Response.STATUS_OK; 428 | 429 | } 430 | 431 | /* ********************************************************************************** */ 432 | /* ********************************* Method OPTIONS ********************************* */ 433 | /* ********************************************************************************** */ 434 | else if (request.method.equalsIgnoreCase("OPTIONS")) { 435 | response.status = Response.STATUS_OK; 436 | response.attributes = "Public: DESCRIBE,SETUP,TEARDOWN,PLAY,PAUSE\r\n"; 437 | response.status = Response.STATUS_OK; 438 | } 439 | 440 | /* ********************************************************************************** */ 441 | /* ********************************** Method SETUP ********************************** */ 442 | /* ********************************************************************************** */ 443 | else if (request.method.equalsIgnoreCase("SETUP")) { 444 | Pattern p; Matcher m; 445 | int p2, p1, ssrc, trackId, src[]; 446 | String destination; 447 | 448 | p = Pattern.compile("trackID=(\\w+)",Pattern.CASE_INSENSITIVE); 449 | m = p.matcher(request.uri); 450 | 451 | if (!m.find()) { 452 | response.status = Response.STATUS_BAD_REQUEST; 453 | return response; 454 | } 455 | 456 | trackId = Integer.parseInt(m.group(1)); 457 | 458 | if (!mSession.trackExists(trackId)) { 459 | response.status = Response.STATUS_NOT_FOUND; 460 | return response; 461 | } 462 | 463 | p = Pattern.compile("client_port=(\\d+)-(\\d+)",Pattern.CASE_INSENSITIVE); 464 | m = p.matcher(request.headers.get("transport")); 465 | 466 | if (!m.find()) { 467 | int[] ports = mSession.getTrack(trackId).getDestinationPorts(); 468 | p1 = ports[0]; 469 | p2 = ports[1]; 470 | } 471 | else { 472 | p1 = Integer.parseInt(m.group(1)); 473 | p2 = Integer.parseInt(m.group(2)); 474 | } 475 | 476 | ssrc = mSession.getTrack(trackId).getSSRC(); 477 | src = mSession.getTrack(trackId).getLocalPorts(); 478 | destination = mSession.getDestination(); 479 | 480 | mSession.getTrack(trackId).setDestinationPorts(p1, p2); 481 | 482 | boolean streaming = isStreaming(); 483 | mSession.syncStart(trackId); 484 | if (!streaming && isStreaming()) { 485 | postMessage(MESSAGE_STREAMING_STARTED); 486 | } 487 | 488 | response.attributes = "Transport: RTP/AVP/UDP;"+(InetAddress.getByName(destination).isMulticastAddress()?"multicast":"unicast")+ 489 | ";destination="+mSession.getDestination()+ 490 | ";client_port="+p1+"-"+p2+ 491 | ";server_port="+src[0]+"-"+src[1]+ 492 | ";ssrc="+Integer.toHexString(ssrc)+ 493 | ";mode=play\r\n" + 494 | "Session: "+ "1185d20035702ca" + "\r\n" + 495 | "Cache-Control: no-cache\r\n"; 496 | response.status = Response.STATUS_OK; 497 | 498 | // If no exception has been thrown, we reply with OK 499 | response.status = Response.STATUS_OK; 500 | 501 | } 502 | 503 | /* ********************************************************************************** */ 504 | /* ********************************** Method PLAY *********************************** */ 505 | /* ********************************************************************************** */ 506 | else if (request.method.equalsIgnoreCase("PLAY")) { 507 | String requestAttributes = "RTP-Info: "; 508 | if (mSession.trackExists(0)) requestAttributes += "url=rtsp://"+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/trackID="+0+";seq=0,"; 509 | if (mSession.trackExists(1)) requestAttributes += "url=rtsp://"+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/trackID="+1+";seq=0,"; 510 | requestAttributes = requestAttributes.substring(0, requestAttributes.length()-1) + "\r\nSession: 1185d20035702ca\r\n"; 511 | 512 | response.attributes = requestAttributes; 513 | 514 | // If no exception has been thrown, we reply with OK 515 | response.status = Response.STATUS_OK; 516 | 517 | } 518 | 519 | /* ********************************************************************************** */ 520 | /* ********************************** Method PAUSE ********************************** */ 521 | /* ********************************************************************************** */ 522 | else if (request.method.equalsIgnoreCase("PAUSE")) { 523 | response.status = Response.STATUS_OK; 524 | } 525 | 526 | /* ********************************************************************************** */ 527 | /* ********************************* Method TEARDOWN ******************************** */ 528 | /* ********************************************************************************** */ 529 | else if (request.method.equalsIgnoreCase("TEARDOWN")) { 530 | response.status = Response.STATUS_OK; 531 | } 532 | 533 | /* ********************************************************************************** */ 534 | /* ********************************* Unknown method ? ******************************* */ 535 | /* ********************************************************************************** */ 536 | else { 537 | Log.e(TAG,"Command unknown: "+request); 538 | response.status = Response.STATUS_BAD_REQUEST; 539 | } 540 | 541 | return response; 542 | 543 | } 544 | 545 | } 546 | 547 | static class Request { 548 | 549 | // Parse method & uri 550 | public static final Pattern regexMethod = Pattern.compile("(\\w+) (\\S+) RTSP",Pattern.CASE_INSENSITIVE); 551 | // Parse a request header 552 | public static final Pattern rexegHeader = Pattern.compile("(\\S+):(.+)",Pattern.CASE_INSENSITIVE); 553 | 554 | public String method; 555 | public String uri; 556 | public HashMap headers = new HashMap(); 557 | 558 | /** Parse the method, uri & headers of a RTSP request */ 559 | public static Request parseRequest(BufferedReader input) throws IOException, IllegalStateException, SocketException { 560 | Request request = new Request(); 561 | String line; 562 | Matcher matcher; 563 | 564 | // Parsing request method & uri 565 | if ((line = input.readLine())==null) throw new SocketException("Client disconnected"); 566 | matcher = regexMethod.matcher(line); 567 | matcher.find(); 568 | request.method = matcher.group(1); 569 | request.uri = matcher.group(2); 570 | 571 | // Parsing headers of the request 572 | while ( (line = input.readLine()) != null && line.length()>3 ) { 573 | matcher = rexegHeader.matcher(line); 574 | matcher.find(); 575 | request.headers.put(matcher.group(1).toLowerCase(Locale.US),matcher.group(2)); 576 | } 577 | if (line==null) throw new SocketException("Client disconnected"); 578 | 579 | // It's not an error, it's just easier to follow what's happening in logcat with the request in red 580 | Log.e(TAG,request.method+" "+request.uri); 581 | 582 | return request; 583 | } 584 | } 585 | 586 | static class Response { 587 | 588 | // Status code definitions 589 | public static final String STATUS_OK = "200 OK"; 590 | public static final String STATUS_BAD_REQUEST = "400 Bad Request"; 591 | public static final String STATUS_NOT_FOUND = "404 Not Found"; 592 | public static final String STATUS_INTERNAL_SERVER_ERROR = "500 Internal Server Error"; 593 | 594 | public String status = STATUS_INTERNAL_SERVER_ERROR; 595 | public String content = ""; 596 | public String attributes = ""; 597 | 598 | private final Request mRequest; 599 | 600 | public Response(Request request) { 601 | this.mRequest = request; 602 | } 603 | 604 | public Response() { 605 | // Be carefull if you modify the send() method because request might be null ! 606 | mRequest = null; 607 | } 608 | 609 | public void send(OutputStream output) throws IOException { 610 | int seqid = -1; 611 | 612 | try { 613 | seqid = Integer.parseInt(mRequest.headers.get("cseq").replace(" ","")); 614 | } catch (Exception e) { 615 | Log.e(TAG,"Error parsing CSeq: "+(e.getMessage()!=null?e.getMessage():"")); 616 | } 617 | 618 | String response = "RTSP/1.0 "+status+"\r\n" + 619 | "Server: "+SERVER_NAME+"\r\n" + 620 | (seqid>=0?("Cseq: " + seqid + "\r\n"):"") + 621 | "Content-Length: " + content.length() + "\r\n" + 622 | attributes + 623 | "\r\n" + 624 | content; 625 | 626 | 627 | Log.d(TAG,response.replace("\r", "")); 628 | 629 | output.write(response.getBytes()); 630 | } 631 | } 632 | 633 | } 634 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/ScreenInputStream.java: -------------------------------------------------------------------------------- 1 | package com.zpf.androidshow.rtsp; 2 | 3 | import com.zpf.androidshow.MainActivity; 4 | import com.zpf.androidshow.media.h264data; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.nio.ByteBuffer; 9 | 10 | /** 11 | * Created by user111 on 2018/3/14. 12 | */ 13 | 14 | public class ScreenInputStream extends InputStream { 15 | 16 | private long ts = 0; 17 | private ByteBuffer mBuffer = null; 18 | private h264data data = null; 19 | 20 | @Override 21 | public int read(byte[] buffer, int offset, int length) throws IOException{ 22 | int min = 0; 23 | 24 | if(mBuffer == null){ 25 | data = MainActivity.h264Queue.poll(); 26 | if(data == null) return 0; 27 | ts = data.ts; 28 | mBuffer =ByteBuffer.wrap(data.data); 29 | mBuffer.position(0); 30 | } 31 | min = length < data.data.length - mBuffer.position() ? length : data.data.length - mBuffer.position(); 32 | mBuffer.get(buffer, offset, min); 33 | if (mBuffer.position()>=data.data.length) { 34 | mBuffer = null; 35 | } 36 | return min; 37 | } 38 | 39 | 40 | @Override 41 | public int read() throws IOException { 42 | return 0; 43 | } 44 | 45 | public int available() { 46 | if (mBuffer != null) 47 | return data.data.length - mBuffer.position(); 48 | else 49 | return 0; 50 | } 51 | 52 | 53 | public long getLastts(){ 54 | return ts; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/ScreenStream.java: -------------------------------------------------------------------------------- 1 | package com.zpf.androidshow.rtsp; 2 | 3 | import android.provider.MediaStore; 4 | 5 | import com.zpf.androidshow.media.VideoMediaCodec; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | 10 | /** 11 | * Created by user111 on 2018/3/14. 12 | */ 13 | 14 | public class ScreenStream extends VideoStream { 15 | 16 | 17 | 18 | public ScreenStream(){ 19 | mPacketizer = new H264Packetizer(); 20 | 21 | } 22 | 23 | public synchronized void start() throws IllegalStateException, IOException { 24 | if (!mStreaming) { 25 | super.start(); 26 | } 27 | } 28 | 29 | /** 30 | * Configures the stream. You need to call this before calling {@link #getSessionDescription()} to apply 31 | * your configuration of the stream. 32 | */ 33 | public synchronized void configure() throws IllegalStateException, IOException { 34 | super.configure(); 35 | 36 | } 37 | 38 | @Override 39 | public String getSessionDescription() throws IllegalStateException { 40 | return "m=video "+String.valueOf(getDestinationPorts()[0])+" RTP/AVP 96\r\n" + 41 | "a=rtpmap:96 H264/90000\r\n" + 42 | "a=fmtp:96 packetization-mode=1;profile-level-id=000042"+";sprop-parameter-sets="+";\r\n"; 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/SenderReport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com 3 | * 4 | * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) 5 | * 6 | * Spydroid is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This source code is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this source code; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | package com.zpf.androidshow.rtsp; 22 | 23 | import android.os.SystemClock; 24 | 25 | import java.io.IOException; 26 | import java.net.DatagramPacket; 27 | import java.net.InetAddress; 28 | import java.net.MulticastSocket; 29 | 30 | /** 31 | * Implementation of Sender Report RTCP packets. 32 | */ 33 | public class SenderReport { 34 | 35 | public static final int MTU = 1500; 36 | 37 | private MulticastSocket usock; 38 | private DatagramPacket upack; 39 | 40 | private byte[] buffer = new byte[MTU]; 41 | private int ssrc, port = -1; 42 | private int octetCount = 0, packetCount = 0; 43 | private long interval, delta, now, oldnow; 44 | 45 | public SenderReport(int ssrc) throws IOException { 46 | super(); 47 | this.ssrc = ssrc; 48 | } 49 | 50 | public SenderReport() { 51 | 52 | /* Version(2) Padding(0) */ 53 | /* ^ ^ PT = 0 */ 54 | /* | | ^ */ 55 | /* | -------- | */ 56 | /* | |--------------------- */ 57 | /* | || */ 58 | /* | || */ 59 | buffer[0] = (byte) Integer.parseInt("10000000",2); 60 | 61 | /* Packet Type PT */ 62 | buffer[1] = (byte) 200; 63 | 64 | /* Byte 2,3 -> Length */ 65 | setLong(28/4-1, 2, 4); 66 | 67 | /* Byte 4,5,6,7 -> SSRC */ 68 | /* Byte 8,9,10,11 -> NTP timestamp hb */ 69 | /* Byte 12,13,14,15 -> NTP timestamp lb */ 70 | /* Byte 16,17,18,19 -> RTP timestamp */ 71 | /* Byte 20,21,22,23 -> packet count */ 72 | /* Byte 24,25,26,27 -> octet count */ 73 | 74 | try { 75 | usock = new MulticastSocket(); 76 | } catch (IOException e) { 77 | throw new RuntimeException(e.getMessage()); 78 | } 79 | upack = new DatagramPacket(buffer, 1); 80 | 81 | // By default we sent one report every 5 secconde 82 | interval = 3000; 83 | 84 | } 85 | 86 | public void close() { 87 | usock.close(); 88 | } 89 | 90 | /** 91 | * Sets the temporal interval between two RTCP Sender Reports. 92 | * Default interval is set to 5 secondes. 93 | * Set 0 to disable RTCP. 94 | * @param interval The interval in milliseconds 95 | */ 96 | public void setInterval(long interval) { 97 | this.interval = interval; 98 | } 99 | 100 | /** 101 | * Updates the number of packets sent, and the total amount of data sent. 102 | * @param length The length of the packet 103 | * @throws IOException 104 | **/ 105 | public void update(int length, long ntpts, long rtpts) throws IOException { 106 | packetCount += 1; 107 | octetCount += length; 108 | setLong(packetCount, 20, 24); 109 | setLong(octetCount, 24, 28); 110 | 111 | now = SystemClock.elapsedRealtime(); 112 | delta += oldnow != 0 ? now-oldnow : 0; 113 | oldnow = now; 114 | if (interval>0) { 115 | if (delta>=interval) { 116 | // We send a Sender Report 117 | send(ntpts,rtpts); 118 | delta = 0; 119 | } 120 | } 121 | 122 | } 123 | 124 | public void setSSRC(int ssrc) { 125 | this.ssrc = ssrc; 126 | setLong(ssrc,4,8); 127 | packetCount = 0; 128 | octetCount = 0; 129 | setLong(packetCount, 20, 24); 130 | setLong(octetCount, 24, 28); 131 | } 132 | 133 | public void setDestination(InetAddress dest, int dport) { 134 | port = dport; 135 | upack.setPort(dport); 136 | upack.setAddress(dest); 137 | } 138 | 139 | public int getPort() { 140 | return port; 141 | } 142 | 143 | public int getLocalPort() { 144 | return usock.getLocalPort(); 145 | } 146 | 147 | public int getSSRC() { 148 | return ssrc; 149 | } 150 | 151 | /** 152 | * Resets the reports (total number of bytes sent, number of packets sent, etc.) 153 | */ 154 | public void reset() { 155 | packetCount = 0; 156 | octetCount = 0; 157 | setLong(packetCount, 20, 24); 158 | setLong(octetCount, 24, 28); 159 | delta = now = oldnow = 0; 160 | } 161 | 162 | private void setLong(long n, int begin, int end) { 163 | for (end--; end >= begin; end--) { 164 | buffer[end] = (byte) (n % 256); 165 | n >>= 8; 166 | } 167 | } 168 | 169 | /** Sends the RTCP packet over the network. */ 170 | private void send(long ntpts, long rtpts) throws IOException { 171 | long hb = ntpts/1000000000; 172 | long lb = ( ( ntpts - hb*1000000000 ) * 4294967296L )/1000000000; 173 | setLong(hb, 8, 12); 174 | setLong(lb, 12, 16); 175 | setLong(rtpts, 16, 20); 176 | upack.setLength(28); 177 | usock.send(upack); 178 | } 179 | 180 | 181 | } 182 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/Session.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com 3 | * 4 | * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) 5 | * 6 | * Spydroid is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This source code is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this source code; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | package com.zpf.androidshow.rtsp; 22 | 23 | import android.os.Handler; 24 | import android.os.HandlerThread; 25 | import android.os.Looper; 26 | 27 | import java.io.IOException; 28 | import java.net.InetAddress; 29 | import java.net.UnknownHostException; 30 | import java.util.concurrent.CountDownLatch; 31 | 32 | /** 33 | * You should instantiate this class with the {@link SessionBuilder}.
34 | * This is the class you will want to use to stream audio and or video to some peer using RTP.
35 | * 36 | * It holds a {@link VideoStream} and a {@link AudioStream} together and provides 37 | * syncronous and asyncrounous functions to start and stop those steams. 38 | * You should implement a callback interface {@link Callback} to receive notifications and error reports.
39 | * 40 | * If you want to stream to a RTSP server, you will need an instance of this class and hand it to 41 | * 42 | * If you don't use the RTSP protocol, you will still need to send a session description to the receiver 43 | * for him to be able to decode your audio/video streams. You can obtain this session description by calling 44 | * {@link #configure()} or {@link #syncConfigure()} to configure the session with its parameters 45 | * (audio samplingrate, video resolution) and then {@link Session#getSessionDescription()}.
46 | * 47 | * See the example 2 here: https://github.com/fyhertz/libstreaming-examples to 48 | * see an example of how to get a SDP.
49 | * 50 | * See the example 3 here: https://github.com/fyhertz/libstreaming-examples to 51 | * see an example of how to stream to a RTSP server.
52 | * 53 | */ 54 | public class Session { 55 | 56 | public final static String TAG = "Session"; 57 | 58 | public final static int STREAM_VIDEO = 0x01; 59 | 60 | public final static int STREAM_AUDIO = 0x00; 61 | 62 | /** Some app is already using a camera (Camera.open() has failed). */ 63 | public final static int ERROR_CAMERA_ALREADY_IN_USE = 0x00; 64 | 65 | /** The phone may not support some streaming parameters that you are using (bit rate, frame rate...s). */ 66 | public final static int ERROR_CONFIGURATION_NOT_SUPPORTED = 0x01; 67 | 68 | /** 69 | * The internal storage of the phone is not ready. 70 | * Libstreaming tried to store a test file on the sdcard but couldn't. 71 | * See H264Stream and AACStream to find out why libstreaming would want to something like that. 72 | */ 73 | public final static int ERROR_STORAGE_NOT_READY = 0x02; 74 | 75 | /** The phone has no flash. */ 76 | public final static int ERROR_CAMERA_HAS_NO_FLASH = 0x03; 77 | 78 | /** The supplied SurfaceView is not a valid surface, or has not been created yet. */ 79 | public final static int ERROR_INVALID_SURFACE = 0x04; 80 | 81 | /** 82 | * The destination set with {@link Session#setDestination(String)} could not be resolved. 83 | * May mean that the phone has no access to the internet, or that the DNS server could not 84 | * resolved the host name. 85 | */ 86 | public final static int ERROR_UNKNOWN_HOST = 0x05; 87 | 88 | /** 89 | * Some other error occured ! 90 | */ 91 | public final static int ERROR_OTHER = 0x06; 92 | 93 | private String mOrigin; 94 | private String mDestination; 95 | private int mTimeToLive = 64; 96 | private long mTimestamp; 97 | 98 | private AudioStream mAudioStream = null; 99 | private VideoStream mVideoStream = null; 100 | 101 | private Callback mCallback; 102 | private Handler mMainHandler; 103 | 104 | private static CountDownLatch sSignal; 105 | private static Handler sHandler; 106 | 107 | static { 108 | // Creates the Thread that will be used when asynchronous methods of a Session are called 109 | sSignal = new CountDownLatch(1); 110 | new HandlerThread("net.majorkernelpanic.streaming.Session"){ 111 | @Override 112 | protected void onLooperPrepared() { 113 | sHandler = new Handler(); 114 | sSignal.countDown(); 115 | } 116 | }.start(); 117 | } 118 | 119 | /** 120 | * Creates a streaming session that can be customized by adding tracks. 121 | */ 122 | public Session() { 123 | long uptime = System.currentTimeMillis(); 124 | mMainHandler = new Handler(Looper.getMainLooper()); 125 | mTimestamp = (uptime/1000)<<32 & (((uptime-((uptime/1000)*1000))>>32)/1000); // NTP timestamp 126 | mOrigin = "127.0.0.1"; 127 | 128 | // Me make sure that we won't send Runnables to a non existing thread 129 | try { 130 | sSignal.await(); 131 | } catch (InterruptedException e) {} 132 | } 133 | 134 | /** 135 | * The callback interface you need to implement to get some feedback 136 | * Those will be called from the UI thread. 137 | */ 138 | public interface Callback { 139 | 140 | /** 141 | * Called periodically to inform you on the bandwidth 142 | * consumption of the streams when streaming. 143 | */ 144 | public void onBitrareUpdate(long bitrate); 145 | 146 | /** Called when some error occurs. */ 147 | public void onSessionError(int reason, int streamType, Exception e); 148 | 149 | /** 150 | * Called when the previw of the {@link VideoStream} 151 | * has correctly been started. 152 | * If an error occurs while starting the preview, 153 | * {@link Callback#onSessionError(int, int, Exception)} will be 154 | * called instead of {@link Callback#onPreviewStarted()}. 155 | */ 156 | public void onPreviewStarted(); 157 | 158 | /** 159 | * Called when the session has correctly been configured 160 | * after calling {@link Session#configure()}. 161 | * If an error occurs while configuring the {@link Session}, 162 | * {@link Callback#onSessionError(int, int, Exception)} will be 163 | * called instead of {@link Callback#onSessionConfigured()}. 164 | */ 165 | public void onSessionConfigured(); 166 | 167 | /** 168 | * Called when the streams of the session have correctly been started. 169 | * If an error occurs while starting the {@link Session}, 170 | * {@link Callback#onSessionError(int, int, Exception)} will be 171 | * called instead of {@link Callback#onSessionStarted()}. 172 | */ 173 | public void onSessionStarted(); 174 | 175 | /** Called when the stream of the session have been stopped. */ 176 | public void onSessionStopped(); 177 | 178 | } 179 | 180 | /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ 181 | void addAudioTrack(AudioStream track) { 182 | removeAudioTrack(); 183 | mAudioStream = track; 184 | } 185 | 186 | /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ 187 | void addVideoTrack(VideoStream track) { 188 | removeVideoTrack(); 189 | mVideoStream = track; 190 | } 191 | 192 | /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ 193 | void removeAudioTrack() { 194 | if (mAudioStream != null) { 195 | mAudioStream.stop(); 196 | mAudioStream = null; 197 | } 198 | } 199 | 200 | /** You probably don't need to use that directly, use the {@link SessionBuilder}. */ 201 | void removeVideoTrack() { 202 | 203 | } 204 | 205 | /** Returns the underlying {@link AudioStream} used by the {@link Session}. */ 206 | public AudioStream getAudioTrack() { 207 | return mAudioStream; 208 | } 209 | 210 | /** Returns the underlying {@link VideoStream} used by the {@link Session}. */ 211 | public VideoStream getVideoTrack() { 212 | return mVideoStream; 213 | } 214 | 215 | /** 216 | * Sets the callback interface that will be called by the {@link Session}. 217 | * @param callback The implementation of the {@link Callback} interface 218 | */ 219 | public void setCallback(Callback callback) { 220 | mCallback = callback; 221 | } 222 | 223 | /** 224 | * The origin address of the session. 225 | * It appears in the sessionn description. 226 | * @param origin The origin address 227 | */ 228 | public void setOrigin(String origin) { 229 | mOrigin = origin; 230 | } 231 | 232 | /** 233 | * The destination address for all the streams of the session. 234 | * Changes will be taken into account the next time you start the session. 235 | * @param destination The destination address 236 | */ 237 | public void setDestination(String destination) { 238 | mDestination = destination; 239 | } 240 | 241 | /** 242 | * Set the TTL of all packets sent during the session. 243 | * Changes will be taken into account the next time you start the session. 244 | * @param ttl The Time To Live 245 | */ 246 | public void setTimeToLive(int ttl) { 247 | mTimeToLive = ttl; 248 | } 249 | 250 | /** 251 | * Returns a Session Description that can be stored in a file or sent to a client with RTSP. 252 | * @return The Session Description. 253 | * @throws IllegalStateException Thrown when {@link #setDestination(String)} has never been called. 254 | */ 255 | public String getSessionDescription() { 256 | StringBuilder sessionDescription = new StringBuilder(); 257 | if (mDestination==null) { 258 | throw new IllegalStateException("setDestination() has not been called !"); 259 | } 260 | sessionDescription.append("v=0\r\n"); 261 | // TODO: Add IPV6 support 262 | sessionDescription.append("o=- "+mTimestamp+" "+mTimestamp+" IN IP4 "+mOrigin+"\r\n"); 263 | sessionDescription.append("s=Unnamed\r\n"); 264 | sessionDescription.append("i=N/A\r\n"); 265 | sessionDescription.append("c=IN IP4 "+mDestination+"\r\n"); 266 | // t=0 0 means the session is permanent (we don't know when it will stop) 267 | sessionDescription.append("t=0 0\r\n"); 268 | sessionDescription.append("a=recvonly\r\n"); 269 | // Prevents two different sessions from using the same peripheral at the same time 270 | if (mAudioStream != null) { 271 | sessionDescription.append(mAudioStream.getSessionDescription()); 272 | sessionDescription.append("a=control:trackID="+0+"\r\n"); 273 | } 274 | if (mVideoStream != null) { 275 | sessionDescription.append(mVideoStream.getSessionDescription()); 276 | sessionDescription.append("a=control:trackID="+1+"\r\n"); 277 | } 278 | return sessionDescription.toString(); 279 | } 280 | 281 | /** Returns the destination set with {@link #setDestination(String)}. */ 282 | public String getDestination() { 283 | return mDestination; 284 | } 285 | 286 | /** Returns an approximation of the bandwidth consumed by the session in bit per seconde. */ 287 | public long getBitrate() { 288 | long sum = 0; 289 | if (mAudioStream != null) sum += mAudioStream.getBitrate(); 290 | if (mVideoStream != null) sum += mVideoStream.getBitrate(); 291 | return sum; 292 | } 293 | 294 | /** Indicates if a track is currently running. */ 295 | public boolean isStreaming() { 296 | if ( (mAudioStream!=null && mAudioStream.isStreaming()) || (mVideoStream!=null && mVideoStream.isStreaming()) ) 297 | return true; 298 | else 299 | return false; 300 | } 301 | 302 | /** 303 | * Configures all streams of the session. 304 | **/ 305 | public void configure() { 306 | sHandler.post(new Runnable() { 307 | @Override 308 | public void run() { 309 | try { 310 | syncConfigure(); 311 | } catch (Exception e) {}; 312 | } 313 | }); 314 | } 315 | 316 | /** 317 | * Does the same thing as {@link #configure()}, but in a syncronous manner. 318 | * Throws exceptions in addition to calling a callback 319 | * {@link Callback#onSessionError(int, int, Exception)} when 320 | * an error occurs. 321 | **/ 322 | public void syncConfigure() { 323 | 324 | for (int id=0;id<2;id++) { 325 | Stream stream = id==0 ? mAudioStream : mVideoStream; 326 | if (stream!=null && !stream.isStreaming()) { 327 | try { 328 | stream.configure(); 329 | } catch (Exception e) { 330 | 331 | } 332 | } 333 | } 334 | postSessionConfigured(); 335 | } 336 | 337 | /** 338 | * Asyncronously starts all streams of the session. 339 | **/ 340 | public void start() { 341 | sHandler.post(new Runnable() { 342 | @Override 343 | public void run() { 344 | try { 345 | syncStart(); 346 | } catch (Exception e) {} 347 | } 348 | }); 349 | } 350 | 351 | /** 352 | * Starts a stream in a syncronous manner. 353 | * Throws exceptions in addition to calling a callback. 354 | * @param id The id of the stream to start 355 | **/ 356 | public void syncStart(int id) { 357 | 358 | Stream stream = id==0 ? mAudioStream : mVideoStream; 359 | if (stream!=null && !stream.isStreaming()) { 360 | try { 361 | InetAddress destination = InetAddress.getByName(mDestination); 362 | stream.setTimeToLive(mTimeToLive); 363 | stream.setDestinationAddress(destination); 364 | stream.start(); 365 | if (getTrack(1-id) == null || getTrack(1-id).isStreaming()) { 366 | postSessionStarted(); 367 | } 368 | if (getTrack(1-id) == null || !getTrack(1-id).isStreaming()) { 369 | sHandler.post(mUpdateBitrate); 370 | } 371 | } catch (Exception e) { 372 | postError(ERROR_UNKNOWN_HOST, id, e); 373 | } 374 | } 375 | 376 | } 377 | 378 | /** 379 | * Does the same thing as {@link #start()}, but in a syncronous manner. 380 | * Throws exceptions in addition to calling a callback. 381 | **/ 382 | public void syncStart() { 383 | 384 | syncStart(1); 385 | try { 386 | syncStart(0); 387 | } catch (RuntimeException e) { 388 | syncStop(1); 389 | throw e; 390 | } 391 | } 392 | 393 | /** Stops all existing streams. */ 394 | public void stop() { 395 | sHandler.post(new Runnable() { 396 | @Override 397 | public void run() { 398 | syncStop(); 399 | } 400 | }); 401 | } 402 | 403 | /** 404 | * Stops one stream in a syncronous manner. 405 | * @param id The id of the stream to stop 406 | **/ 407 | private void syncStop(final int id) { 408 | Stream stream = id==0 ? mAudioStream : mVideoStream; 409 | if (stream!=null) { 410 | stream.stop(); 411 | } 412 | } 413 | 414 | /** Stops all existing streams in a syncronous manner. */ 415 | public void syncStop() { 416 | syncStop(0); 417 | syncStop(1); 418 | postSessionStopped(); 419 | } 420 | 421 | 422 | 423 | /** Deletes all existing tracks & release associated resources. */ 424 | public void release() { 425 | removeAudioTrack(); 426 | removeVideoTrack(); 427 | sHandler.getLooper().quit(); 428 | } 429 | 430 | private void postSessionConfigured() { 431 | mMainHandler.post(new Runnable() { 432 | @Override 433 | public void run() { 434 | if (mCallback != null) { 435 | mCallback.onSessionConfigured(); 436 | } 437 | } 438 | }); 439 | } 440 | 441 | private void postSessionStarted() { 442 | mMainHandler.post(new Runnable() { 443 | @Override 444 | public void run() { 445 | if (mCallback != null) { 446 | mCallback.onSessionStarted(); 447 | } 448 | } 449 | }); 450 | } 451 | 452 | private void postSessionStopped() { 453 | mMainHandler.post(new Runnable() { 454 | @Override 455 | public void run() { 456 | if (mCallback != null) { 457 | mCallback.onSessionStopped(); 458 | } 459 | } 460 | }); 461 | } 462 | 463 | private void postError(final int reason, final int streamType,final Exception e) { 464 | mMainHandler.post(new Runnable() { 465 | @Override 466 | public void run() { 467 | if (mCallback != null) { 468 | mCallback.onSessionError(reason, streamType, e); 469 | } 470 | } 471 | }); 472 | } 473 | 474 | private void postBitRate(final long bitrate) { 475 | mMainHandler.post(new Runnable() { 476 | @Override 477 | public void run() { 478 | if (mCallback != null) { 479 | mCallback.onBitrareUpdate(bitrate); 480 | } 481 | } 482 | }); 483 | } 484 | 485 | private Runnable mUpdateBitrate = new Runnable() { 486 | @Override 487 | public void run() { 488 | if (isStreaming()) { 489 | postBitRate(getBitrate()); 490 | sHandler.postDelayed(mUpdateBitrate, 500); 491 | } else { 492 | postBitRate(0); 493 | } 494 | } 495 | }; 496 | 497 | 498 | public boolean trackExists(int id) { 499 | if (id==0) 500 | return mAudioStream!=null; 501 | else 502 | return mVideoStream!=null; 503 | } 504 | 505 | public Stream getTrack(int id) { 506 | if (id==0) 507 | return mAudioStream; 508 | else 509 | return mVideoStream; 510 | } 511 | 512 | } 513 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/SessionBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com 3 | * 4 | * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) 5 | * 6 | * Spydroid is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This source code is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this source code; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | package com.zpf.androidshow.rtsp; 22 | 23 | import android.content.Context; 24 | import android.hardware.Camera.CameraInfo; 25 | import android.preference.PreferenceManager; 26 | 27 | 28 | import java.io.IOException; 29 | 30 | /** 31 | * Call {@link #getInstance()} to get access to the SessionBuilder. 32 | */ 33 | public class SessionBuilder { 34 | 35 | public final static String TAG = "SessionBuilder"; 36 | 37 | /** Can be used with {@link #setVideoEncoder}. */ 38 | public final static int VIDEO_NONE = 0; 39 | 40 | /** Can be used with {@link #setVideoEncoder}. */ 41 | public final static int VIDEO_H264 = 1; 42 | 43 | /** Can be used with {@link #setVideoEncoder}. */ 44 | public final static int VIDEO_H263 = 2; 45 | 46 | /** Can be used with {@link #setAudioEncoder}. */ 47 | public final static int AUDIO_NONE = 0; 48 | 49 | /** Can be used with {@link #setAudioEncoder}. */ 50 | public final static int AUDIO_AMRNB = 3; 51 | 52 | /** Can be used with {@link #setAudioEncoder}. */ 53 | public final static int AUDIO_AAC = 5; 54 | 55 | // Default configuration 56 | private VideoQuality mVideoQuality = VideoQuality.DEFAULT_VIDEO_QUALITY; 57 | private AudioQuality mAudioQuality = AudioQuality.DEFAULT_AUDIO_QUALITY; 58 | private Context mContext; 59 | private int mVideoEncoder = VIDEO_H264; 60 | private int mAudioEncoder = AUDIO_AAC; 61 | private int mCamera = CameraInfo.CAMERA_FACING_BACK; 62 | private int mTimeToLive = 64; 63 | private int mOrientation = 0; 64 | private boolean mFlash = false; 65 | private String mOrigin = null; 66 | private String mDestination = null; 67 | private Session.Callback mCallback = null; 68 | 69 | // Removes the default public constructor 70 | private SessionBuilder() {} 71 | 72 | // The SessionManager implements the singleton pattern 73 | private static volatile SessionBuilder sInstance = null; 74 | 75 | /** 76 | * Returns a reference to the {@link SessionBuilder}. 77 | * @return The reference to the {@link SessionBuilder} 78 | */ 79 | public final static SessionBuilder getInstance() { 80 | if (sInstance == null) { 81 | synchronized (SessionBuilder.class) { 82 | if (sInstance == null) { 83 | SessionBuilder.sInstance = new SessionBuilder(); 84 | } 85 | } 86 | } 87 | return sInstance; 88 | } 89 | 90 | /** 91 | * Creates a new {@link Session}. 92 | * @return The new Session 93 | * @throws IOException 94 | */ 95 | public Session build() { 96 | Session session; 97 | 98 | session = new Session(); 99 | session.setOrigin(mOrigin); 100 | session.setDestination(mDestination); 101 | session.setTimeToLive(mTimeToLive); 102 | session.setCallback(mCallback); 103 | 104 | switch (mAudioEncoder) { 105 | case AUDIO_AAC: 106 | // AACStream stream = new AACStream(); 107 | // session.addAudioTrack(stream); 108 | // if (mContext!=null) 109 | // stream.setPreferences(PreferenceManager.getDefaultSharedPreferences(mContext)); 110 | break; 111 | case AUDIO_AMRNB: 112 | //session.addAudioTrack(new AMRNBStream()); 113 | break; 114 | } 115 | 116 | switch (mVideoEncoder) { 117 | case VIDEO_H263: 118 | //session.addVideoTrack(new H263Stream(mCamera)); 119 | break; 120 | case VIDEO_H264: 121 | ScreenStream stream = new ScreenStream(); 122 | if (mContext!=null) 123 | stream.setPreferences(PreferenceManager.getDefaultSharedPreferences(mContext)); 124 | session.addVideoTrack(stream); 125 | break; 126 | } 127 | 128 | if (session.getVideoTrack()!=null) { 129 | VideoStream video = session.getVideoTrack(); 130 | //video.setFlashState(mFlash); 131 | //video.setVideoQuality(mVideoQuality); 132 | //video.setSurfaceView(mSurfaceView); 133 | //video.setPreviewOrientation(mOrientation); 134 | //video.setDestinationPorts(5006); 135 | } 136 | 137 | if (session.getAudioTrack()!=null) { 138 | AudioStream audio = session.getAudioTrack(); 139 | audio.setAudioQuality(mAudioQuality); 140 | audio.setDestinationPorts(5004); 141 | } 142 | 143 | return session; 144 | 145 | } 146 | 147 | /** 148 | * Access to the context is needed for the H264Stream class to store some stuff in the SharedPreferences. 149 | * Note that you should pass the Application context, not the context of an Activity. 150 | **/ 151 | public SessionBuilder setContext(Context context) { 152 | mContext = context; 153 | return this; 154 | } 155 | 156 | /** Sets the destination of the session. */ 157 | public SessionBuilder setDestination(String destination) { 158 | mDestination = destination; 159 | return this; 160 | } 161 | 162 | /** Sets the origin of the session. It appears in the SDP of the session. */ 163 | public SessionBuilder setOrigin(String origin) { 164 | mOrigin = origin; 165 | return this; 166 | } 167 | 168 | /** Sets the video stream quality. */ 169 | public SessionBuilder setVideoQuality(VideoQuality quality) { 170 | mVideoQuality = quality.clone(); 171 | return this; 172 | } 173 | 174 | /** Sets the audio encoder. */ 175 | public SessionBuilder setAudioEncoder(int encoder) { 176 | mAudioEncoder = encoder; 177 | return this; 178 | } 179 | 180 | /** Sets the audio quality. */ 181 | public SessionBuilder setAudioQuality(AudioQuality quality) { 182 | mAudioQuality = quality.clone(); 183 | return this; 184 | } 185 | 186 | /** Sets the default video encoder. */ 187 | public SessionBuilder setVideoEncoder(int encoder) { 188 | mVideoEncoder = encoder; 189 | return this; 190 | } 191 | 192 | public SessionBuilder setFlashEnabled(boolean enabled) { 193 | mFlash = enabled; 194 | return this; 195 | } 196 | 197 | public SessionBuilder setCamera(int camera) { 198 | mCamera = camera; 199 | return this; 200 | } 201 | 202 | public SessionBuilder setTimeToLive(int ttl) { 203 | mTimeToLive = ttl; 204 | return this; 205 | } 206 | 207 | /** 208 | * Sets the orientation of the preview. 209 | * @param orientation The orientation of the preview 210 | */ 211 | public SessionBuilder setPreviewOrientation(int orientation) { 212 | mOrientation = orientation; 213 | return this; 214 | } 215 | 216 | public SessionBuilder setCallback(Session.Callback callback) { 217 | mCallback = callback; 218 | return this; 219 | } 220 | 221 | /** Returns the context set with {@link #setContext(Context)}*/ 222 | public Context getContext() { 223 | return mContext; 224 | } 225 | 226 | /** Returns the destination ip address set with {@link #setDestination(String)}. */ 227 | public String getDestination() { 228 | return mDestination; 229 | } 230 | 231 | /** Returns the origin ip address set with {@link #setOrigin(String)}. */ 232 | public String getOrigin() { 233 | return mOrigin; 234 | } 235 | 236 | /** Returns the audio encoder set with {@link #setAudioEncoder(int)}. */ 237 | public int getAudioEncoder() { 238 | return mAudioEncoder; 239 | } 240 | 241 | /** Returns the id of the {@link android.hardware.Camera} set with {@link #setCamera(int)}. */ 242 | public int getCamera() { 243 | return mCamera; 244 | } 245 | 246 | /** Returns the video encoder set with {@link #setVideoEncoder(int)}. */ 247 | public int getVideoEncoder() { 248 | return mVideoEncoder; 249 | } 250 | 251 | /** Returns the VideoQuality set with {@link #setVideoQuality(VideoQuality)}. */ 252 | public VideoQuality getVideoQuality() { 253 | return mVideoQuality; 254 | } 255 | 256 | /** Returns the AudioQuality set with {@link #setAudioQuality(AudioQuality)}. */ 257 | public AudioQuality getAudioQuality() { 258 | return mAudioQuality; 259 | } 260 | 261 | /** Returns the flash state set with {@link #setFlashEnabled(boolean)}. */ 262 | public boolean getFlashState() { 263 | return mFlash; 264 | } 265 | 266 | 267 | /** Returns the time to live set with {@link #setTimeToLive(int)}. */ 268 | public int getTimeToLive() { 269 | return mTimeToLive; 270 | } 271 | 272 | /** Returns a new {@link SessionBuilder} with the same configuration. */ 273 | public SessionBuilder clone() { 274 | return new SessionBuilder() 275 | .setDestination(mDestination) 276 | .setOrigin(mOrigin) 277 | .setPreviewOrientation(mOrientation) 278 | .setVideoQuality(mVideoQuality) 279 | .setVideoEncoder(mVideoEncoder) 280 | .setFlashEnabled(mFlash) 281 | .setCamera(mCamera) 282 | .setTimeToLive(mTimeToLive) 283 | .setAudioEncoder(mAudioEncoder) 284 | .setAudioQuality(mAudioQuality) 285 | .setContext(mContext) 286 | .setCallback(mCallback); 287 | } 288 | 289 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/Stream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com 3 | * 4 | * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) 5 | * 6 | * Spydroid is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This source code is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this source code; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | package com.zpf.androidshow.rtsp; 22 | 23 | import java.io.IOException; 24 | import java.net.InetAddress; 25 | 26 | /** 27 | * An interface that represents a Stream. 28 | */ 29 | public interface Stream { 30 | 31 | /** 32 | * Configures the stream. You need to call this before calling {@link #getSessionDescription()} 33 | * to apply your configuration of the stream. 34 | */ 35 | public void configure() throws IllegalStateException, IOException; 36 | 37 | /** 38 | * Starts the stream. 39 | * This method can only be called after {@link Stream#configure()}. 40 | */ 41 | public void start() throws IllegalStateException, IOException; 42 | 43 | /** 44 | * Stops the stream. 45 | */ 46 | public void stop(); 47 | 48 | /** 49 | * Sets the Time To Live of packets sent over the network. 50 | * @param ttl The time to live 51 | * @throws IOException 52 | */ 53 | public void setTimeToLive(int ttl) throws IOException; 54 | 55 | /** 56 | * Sets the destination ip address of the stream. 57 | * @param dest The destination address of the stream 58 | */ 59 | public void setDestinationAddress(InetAddress dest); 60 | 61 | /** 62 | * Sets the destination ports of the stream. 63 | * If an odd number is supplied for the destination port then the next 64 | * lower even number will be used for RTP and it will be used for RTCP. 65 | * If an even number is supplied, it will be used for RTP and the next odd 66 | * number will be used for RTCP. 67 | * @param dport The destination port 68 | */ 69 | public void setDestinationPorts(int dport); 70 | 71 | /** 72 | * Sets the destination ports of the stream. 73 | * @param rtpPort Destination port that will be used for RTP 74 | * @param rtcpPort Destination port that will be used for RTCP 75 | */ 76 | public void setDestinationPorts(int rtpPort, int rtcpPort); 77 | 78 | /** 79 | * Returns a pair of source ports, the first one is the 80 | * one used for RTP and the second one is used for RTCP. 81 | **/ 82 | public int[] getLocalPorts(); 83 | 84 | /** 85 | * Returns a pair of destination ports, the first one is the 86 | * one used for RTP and the second one is used for RTCP. 87 | **/ 88 | public int[] getDestinationPorts(); 89 | 90 | 91 | /** 92 | * Returns the SSRC of the underlying 93 | * @return the SSRC of the stream. 94 | */ 95 | public int getSSRC(); 96 | 97 | /** 98 | * Returns an approximation of the bit rate consumed by the stream in bit per seconde. 99 | */ 100 | public long getBitrate(); 101 | 102 | /** 103 | * Returns a description of the stream using SDP. 104 | * This method can only be called after {@link Stream#configure()}. 105 | * @throws IllegalStateException Thrown when {@link Stream#configure()} wa not called. 106 | */ 107 | public String getSessionDescription() throws IllegalStateException; 108 | 109 | public boolean isStreaming(); 110 | 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/UriParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com 3 | * 4 | * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) 5 | * 6 | * Spydroid is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This source code is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this source code; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | package com.zpf.androidshow.rtsp; 22 | 23 | import android.hardware.Camera.CameraInfo; 24 | 25 | import org.apache.http.NameValuePair; 26 | import org.apache.http.client.utils.URLEncodedUtils; 27 | 28 | import java.io.IOException; 29 | import java.net.InetAddress; 30 | import java.net.URI; 31 | import java.net.UnknownHostException; 32 | import java.util.Iterator; 33 | import java.util.List; 34 | 35 | import static com.zpf.androidshow.rtsp.SessionBuilder.*; 36 | 37 | /** 38 | * This class parses URIs received by the RTSP server and configures a Session accordingly. 39 | */ 40 | public class UriParser { 41 | 42 | public final static String TAG = "UriParser"; 43 | 44 | /** 45 | * Configures a Session according to the given URI. 46 | * Here are some examples of URIs that can be used to configure a Session: 47 | *
  • rtsp://xxx.xxx.xxx.xxx:8086?h264&flash=on
  • 48 | *
  • rtsp://xxx.xxx.xxx.xxx:8086?h263&camera=front&flash=on
  • 49 | *
  • rtsp://xxx.xxx.xxx.xxx:8086?h264=200-20-320-240
  • 50 | *
  • rtsp://xxx.xxx.xxx.xxx:8086?aac
51 | * @param uri The URI 52 | * @throws IllegalStateException 53 | * @throws IOException 54 | * @return A Session configured according to the URI 55 | */ 56 | public static Session parse(String uri) throws IllegalStateException, IOException { 57 | SessionBuilder builder = SessionBuilder.getInstance().clone(); 58 | byte audioApi = 0, videoApi = 0; 59 | 60 | List params = URLEncodedUtils.parse(URI.create(uri),"UTF-8"); 61 | if (params.size()>0) { 62 | 63 | builder.setAudioEncoder(AUDIO_NONE).setVideoEncoder(VIDEO_NONE); 64 | 65 | // Those parameters must be parsed first or else they won't necessarily be taken into account 66 | for (Iterator it = params.iterator();it.hasNext();) { 67 | NameValuePair param = it.next(); 68 | 69 | // FLASH ON/OFF 70 | if (param.getName().equalsIgnoreCase("flash")) { 71 | if (param.getValue().equalsIgnoreCase("on")) 72 | builder.setFlashEnabled(true); 73 | else 74 | builder.setFlashEnabled(false); 75 | } 76 | 77 | // CAMERA -> the client can choose between the front facing camera and the back facing camera 78 | else if (param.getName().equalsIgnoreCase("camera")) { 79 | if (param.getValue().equalsIgnoreCase("back")) 80 | builder.setCamera(CameraInfo.CAMERA_FACING_BACK); 81 | else if (param.getValue().equalsIgnoreCase("front")) 82 | builder.setCamera(CameraInfo.CAMERA_FACING_FRONT); 83 | } 84 | 85 | // MULTICAST -> the stream will be sent to a multicast group 86 | // The default mutlicast address is 228.5.6.7, but the client can specify another 87 | else if (param.getName().equalsIgnoreCase("multicast")) { 88 | if (param.getValue()!=null) { 89 | try { 90 | InetAddress addr = InetAddress.getByName(param.getValue()); 91 | if (!addr.isMulticastAddress()) { 92 | throw new IllegalStateException("Invalid multicast address !"); 93 | } 94 | builder.setDestination(param.getValue()); 95 | } catch (UnknownHostException e) { 96 | throw new IllegalStateException("Invalid multicast address !"); 97 | } 98 | } 99 | else { 100 | // Default multicast address 101 | builder.setDestination("228.5.6.7"); 102 | } 103 | } 104 | 105 | // UNICAST -> the client can use this to specify where he wants the stream to be sent 106 | else if (param.getName().equalsIgnoreCase("unicast")) { 107 | if (param.getValue()!=null) { 108 | builder.setDestination(param.getValue()); 109 | } 110 | } 111 | 112 | // VIDEOAPI -> can be used to specify what api will be used to encode video (the MediaRecorder API or the MediaCodec API) 113 | else if (param.getName().equalsIgnoreCase("videoapi")) { 114 | if (param.getValue()!=null) { 115 | if (param.getValue().equalsIgnoreCase("mr")) { 116 | videoApi = MediaStream.MODE_MEDIARECORDER_API; 117 | } else if (param.getValue().equalsIgnoreCase("mc")) { 118 | videoApi = MediaStream.MODE_MEDIACODEC_API; 119 | } 120 | } 121 | } 122 | 123 | // AUDIOAPI -> can be used to specify what api will be used to encode audio (the MediaRecorder API or the MediaCodec API) 124 | else if (param.getName().equalsIgnoreCase("audioapi")) { 125 | if (param.getValue()!=null) { 126 | if (param.getValue().equalsIgnoreCase("mr")) { 127 | audioApi = MediaStream.MODE_MEDIARECORDER_API; 128 | } else if (param.getValue().equalsIgnoreCase("mc")) { 129 | audioApi = MediaStream.MODE_MEDIACODEC_API; 130 | } 131 | } 132 | } 133 | 134 | // TTL -> the client can modify the time to live of packets 135 | // By default ttl=64 136 | else if (param.getName().equalsIgnoreCase("ttl")) { 137 | if (param.getValue()!=null) { 138 | try { 139 | int ttl = Integer.parseInt(param.getValue()); 140 | if (ttl<0) throw new IllegalStateException(); 141 | builder.setTimeToLive(ttl); 142 | } catch (Exception e) { 143 | throw new IllegalStateException("The TTL must be a positive integer !"); 144 | } 145 | } 146 | } 147 | 148 | // H.264 149 | else if (param.getName().equalsIgnoreCase("h264")) { 150 | VideoQuality quality = VideoQuality.parseQuality(param.getValue()); 151 | builder.setVideoQuality(quality).setVideoEncoder(VIDEO_H264); 152 | } 153 | 154 | // H.263 155 | else if (param.getName().equalsIgnoreCase("h263")) { 156 | VideoQuality quality = VideoQuality.parseQuality(param.getValue()); 157 | builder.setVideoQuality(quality).setVideoEncoder(VIDEO_H263); 158 | } 159 | 160 | // AMR 161 | else if (param.getName().equalsIgnoreCase("amrnb") || param.getName().equalsIgnoreCase("amr")) { 162 | AudioQuality quality = AudioQuality.parseQuality(param.getValue()); 163 | builder.setAudioQuality(quality).setAudioEncoder(AUDIO_AMRNB); 164 | } 165 | 166 | // AAC 167 | else if (param.getName().equalsIgnoreCase("aac")) { 168 | AudioQuality quality = AudioQuality.parseQuality(param.getValue()); 169 | builder.setAudioQuality(quality).setAudioEncoder(AUDIO_AAC); 170 | } 171 | 172 | } 173 | 174 | } 175 | 176 | if (builder.getVideoEncoder()==VIDEO_NONE && builder.getAudioEncoder()==AUDIO_NONE) { 177 | SessionBuilder b = SessionBuilder.getInstance(); 178 | builder.setVideoEncoder(b.getVideoEncoder()); 179 | builder.setAudioEncoder(b.getAudioEncoder()); 180 | } 181 | 182 | Session session = builder.build(); 183 | 184 | if (videoApi>0 && session.getVideoTrack() != null) { 185 | session.getVideoTrack().setStreamingMethod(videoApi); 186 | } 187 | 188 | if (audioApi>0 && session.getAudioTrack() != null) { 189 | session.getAudioTrack().setStreamingMethod(audioApi); 190 | } 191 | 192 | return session; 193 | 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/VideoQuality.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com 3 | * 4 | * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) 5 | * 6 | * Spydroid is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This source code is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this source code; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | package com.zpf.androidshow.rtsp; 22 | 23 | import android.hardware.Camera; 24 | import android.hardware.Camera.Size; 25 | import android.util.Log; 26 | 27 | import java.util.Iterator; 28 | import java.util.List; 29 | 30 | /** 31 | * A class that represents the quality of a video stream. 32 | * It contains the resolution, the framerate (in fps) and the bitrate (in bps) of the stream. 33 | */ 34 | public class VideoQuality { 35 | 36 | public final static String TAG = "VideoQuality"; 37 | 38 | /** Default video stream quality. */ 39 | public final static VideoQuality DEFAULT_VIDEO_QUALITY = new VideoQuality(176,144,20,500000); 40 | 41 | /** Represents a quality for a video stream. */ 42 | public VideoQuality() {} 43 | 44 | /** 45 | * Represents a quality for a video stream. 46 | * @param resX The horizontal resolution 47 | * @param resY The vertical resolution 48 | */ 49 | public VideoQuality(int resX, int resY) { 50 | this.resX = resX; 51 | this.resY = resY; 52 | } 53 | 54 | /** 55 | * Represents a quality for a video stream. 56 | * @param resX The horizontal resolution 57 | * @param resY The vertical resolution 58 | * @param framerate The framerate in frame per seconds 59 | * @param bitrate The bitrate in bit per seconds 60 | */ 61 | public VideoQuality(int resX, int resY, int framerate, int bitrate) { 62 | this.framerate = framerate; 63 | this.bitrate = bitrate; 64 | this.resX = resX; 65 | this.resY = resY; 66 | } 67 | 68 | public int framerate = 0; 69 | public int bitrate = 0; 70 | public int resX = 0; 71 | public int resY = 0; 72 | 73 | public boolean equals(VideoQuality quality) { 74 | if (quality==null) return false; 75 | return (quality.resX == this.resX & 76 | quality.resY == this.resY & 77 | quality.framerate == this.framerate & 78 | quality.bitrate == this.bitrate); 79 | } 80 | 81 | public VideoQuality clone() { 82 | return new VideoQuality(resX,resY,framerate,bitrate); 83 | } 84 | 85 | public static VideoQuality parseQuality(String str) { 86 | VideoQuality quality = DEFAULT_VIDEO_QUALITY.clone(); 87 | if (str != null) { 88 | String[] config = str.split("-"); 89 | try { 90 | quality.bitrate = Integer.parseInt(config[0])*1000; // conversion to bit/s 91 | quality.framerate = Integer.parseInt(config[1]); 92 | quality.resX = Integer.parseInt(config[2]); 93 | quality.resY = Integer.parseInt(config[3]); 94 | } 95 | catch (IndexOutOfBoundsException ignore) {} 96 | } 97 | return quality; 98 | } 99 | 100 | /** 101 | * Checks if the requested resolution is supported by the camera. 102 | * If not, it modifies it by supported parameters. 103 | **/ 104 | public static VideoQuality determineClosestSupportedResolution(Camera.Parameters parameters, VideoQuality quality) { 105 | VideoQuality v = quality.clone(); 106 | int minDist = Integer.MAX_VALUE; 107 | String supportedSizesStr = "Supported resolutions: "; 108 | List supportedSizes = parameters.getSupportedPreviewSizes(); 109 | for (Iterator it = supportedSizes.iterator(); it.hasNext();) { 110 | Size size = it.next(); 111 | supportedSizesStr += size.width+"x"+size.height+(it.hasNext()?", ":""); 112 | int dist = Math.abs(quality.resX - size.width); 113 | if (dist"+v.resX+"x"+v.resY); 122 | } 123 | 124 | return v; 125 | } 126 | 127 | public static int[] determineMaximumSupportedFramerate(Camera.Parameters parameters) { 128 | int[] maxFps = new int[]{0,0}; 129 | String supportedFpsRangesStr = "Supported frame rates: "; 130 | List supportedFpsRanges = parameters.getSupportedPreviewFpsRange(); 131 | for (Iterator it = supportedFpsRanges.iterator(); it.hasNext();) { 132 | int[] interval = it.next(); 133 | supportedFpsRangesStr += interval[0]/1000+"-"+interval[1]/1000+"fps"+(it.hasNext()?", ":""); 134 | if (interval[1]>maxFps[1] || (interval[0]>maxFps[0] && interval[1]==maxFps[1])) { 135 | maxFps = interval; 136 | } 137 | } 138 | Log.v(TAG,supportedFpsRangesStr); 139 | return maxFps; 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/rtsp/VideoStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com 3 | * 4 | * This file is part of libstreaming (https://github.com/fyhertz/libstreaming) 5 | * 6 | * Spydroid is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This source code is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this source code; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | package com.zpf.androidshow.rtsp; 22 | 23 | import android.annotation.SuppressLint; 24 | import android.content.SharedPreferences; 25 | import java.io.IOException; 26 | 27 | /** 28 | * Don't use this class directly. 29 | */ 30 | public abstract class VideoStream extends MediaStream { 31 | 32 | protected final static String TAG = "VideoStream"; 33 | protected SharedPreferences mSettings = null; 34 | 35 | /** 36 | * Don't use this class directly. 37 | * Uses CAMERA_FACING_BACK by default. 38 | */ 39 | public VideoStream() { 40 | 41 | } 42 | 43 | /** 44 | * Some data (SPS and PPS params) needs to be stored when {@link #getSessionDescription()} is called 45 | * @param prefs The SharedPreferences that will be used to save SPS and PPS parameters 46 | */ 47 | public void setPreferences(SharedPreferences prefs) { 48 | mSettings = prefs; 49 | } 50 | 51 | /** 52 | * Configures the stream. You need to call this before calling {@link #getSessionDescription()} 53 | * to apply your configuration of the stream. 54 | */ 55 | public synchronized void configure() throws IllegalStateException, IOException { 56 | super.configure(); 57 | } 58 | 59 | 60 | public synchronized void start() throws IllegalStateException, IOException { 61 | super.start(); 62 | } 63 | 64 | /** Stops the stream. */ 65 | public synchronized void stop() { 66 | 67 | } 68 | 69 | /** 70 | * Video encoding is done by a MediaRecorder. 71 | */ 72 | protected void encodeWithMediaRecorder() throws IOException { 73 | 74 | 75 | 76 | } 77 | 78 | 79 | /** 80 | * Video encoding is done by a MediaCodec. 81 | */ 82 | protected void encodeWithMediaCodec() throws RuntimeException, IOException { 83 | // The packetizer encapsulates the bit stream in an RTP stream and send it over the network 84 | mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort); 85 | mPacketizer.setInputStream(new ScreenInputStream()); 86 | mPacketizer.start(); 87 | 88 | mStreaming = true; 89 | } 90 | 91 | /** 92 | * Video encoding is done by a MediaCodec. 93 | */ 94 | @SuppressLint("NewApi") 95 | protected void encodeWithMediaCodecMethod1() throws RuntimeException, IOException { 96 | 97 | 98 | 99 | } 100 | 101 | /** 102 | * Video encoding is done by a MediaCodec. 103 | * But here we will use the buffer-to-surface methode 104 | */ 105 | @SuppressLint({ "InlinedApi", "NewApi" }) 106 | protected void encodeWithMediaCodecMethod2() throws RuntimeException, IOException { 107 | 108 | 109 | } 110 | 111 | /** 112 | * Returns a description of the stream using SDP. 113 | * This method can only be called after {@link Stream#configure()}. 114 | * @throws IllegalStateException Thrown when {@link Stream#configure()} wa not called. 115 | */ 116 | public abstract String getSessionDescription() throws IllegalStateException; 117 | 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/screen/Constant.java: -------------------------------------------------------------------------------- 1 | package com.zpf.androidshow.screen; 2 | 3 | /** 4 | * Created by zpf on 2018/3/7. 5 | */ 6 | 7 | public class Constant { 8 | 9 | public static final String MIME_TYPE = "video/avc"; 10 | 11 | public static final int VIDEO_WIDTH = 1280; 12 | 13 | public static final int VIDEO_HEIGHT = 720; 14 | 15 | public static final int VIDEO_DPI = 1; 16 | 17 | public static final int VIDEO_BITRATE = 500000; 18 | 19 | public static final int VIDEO_FRAMERATE = 15; 20 | 21 | public static final int VIDEO_IFRAME_INTER = 5; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/zpf/androidshow/screen/ScreenRecord.java: -------------------------------------------------------------------------------- 1 | package com.zpf.androidshow.screen; 2 | 3 | import android.content.Context; 4 | import android.hardware.display.DisplayManager; 5 | import android.hardware.display.VirtualDisplay; 6 | import android.hardware.display.VirtualDisplay.Callback; 7 | import android.media.MediaCodec; 8 | import android.media.MediaCodecInfo; 9 | import android.media.MediaFormat; 10 | import android.media.projection.MediaProjection; 11 | import android.view.Surface; 12 | 13 | import com.zpf.androidshow.media.VideoMediaCodec; 14 | 15 | import java.io.IOException; 16 | 17 | 18 | /** 19 | * Created by zpf on 2018/2/28. 20 | */ 21 | 22 | public class ScreenRecord extends Thread { 23 | 24 | private final static String TAG = "ScreenRecord"; 25 | 26 | private Surface mSurface; 27 | private Context mContext; 28 | private VirtualDisplay mVirtualDisplay; 29 | private MediaProjection mMediaProjection; 30 | 31 | private VideoMediaCodec mVideoMediaCodec; 32 | 33 | public ScreenRecord(Context context,MediaProjection mp){ 34 | this.mContext = context; 35 | this.mMediaProjection = mp; 36 | mVideoMediaCodec = new VideoMediaCodec(); 37 | } 38 | 39 | @Override 40 | public void run() { 41 | mVideoMediaCodec.prepare(); 42 | mSurface = mVideoMediaCodec.getSurface(); 43 | mVirtualDisplay =mMediaProjection.createVirtualDisplay(TAG + "-display", Constant.VIDEO_WIDTH, Constant.VIDEO_HEIGHT, Constant.VIDEO_DPI, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, 44 | mSurface, null, null); 45 | mVideoMediaCodec.isRun(true); 46 | mVideoMediaCodec.getBuffer(); 47 | } 48 | 49 | /** 50 | * 停止 51 | * **/ 52 | public void release(){ 53 | mVideoMediaCodec.release(); 54 | } 55 | 56 | 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 16 | 17 | 23 | 24 | 29 | 30 | 31 |