├── .gitignore ├── .idea ├── caches │ └── build_file_checksums.ser └── misc.xml ├── README ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── test │ │ └── screenimage │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── test │ │ │ └── screenimage │ │ │ ├── MyApplication.java │ │ │ ├── audio │ │ │ ├── AudioEncoder.java │ │ │ ├── AudioProcessor.java │ │ │ └── OnAudioEncodeListener.java │ │ │ ├── configuration │ │ │ ├── AudioConfiguration.java │ │ │ └── VideoConfiguration.java │ │ │ ├── constant │ │ │ ├── Constants.java │ │ │ └── ScreenImageApi.java │ │ │ ├── controller │ │ │ ├── StreamController.java │ │ │ ├── audio │ │ │ │ ├── IAudioController.java │ │ │ │ └── NormalAudioController.java │ │ │ └── video │ │ │ │ ├── IVideoController.java │ │ │ │ └── ScreenVideoController.java │ │ │ ├── core │ │ │ └── BaseActivity.java │ │ │ ├── entity │ │ │ ├── Frame.java │ │ │ ├── ReceiveData.java │ │ │ └── ReceiveHeader.java │ │ │ ├── mediacodec │ │ │ ├── AudioMediaCodec.java │ │ │ └── VideoMediaCodec.java │ │ │ ├── net │ │ │ ├── OnTcpSendMessageListner.java │ │ │ ├── RequestTcp.java │ │ │ ├── TcpUtil.java │ │ │ └── boastcast │ │ │ │ └── NetWorkStateReceiver.java │ │ │ ├── screen │ │ │ └── ScreenRecordEncoder.java │ │ │ ├── service │ │ │ └── ScreenImageService.java │ │ │ ├── stream │ │ │ ├── packer │ │ │ │ ├── AnnexbHelper.java │ │ │ │ ├── Packer.java │ │ │ │ └── TcpPacker.java │ │ │ └── sender │ │ │ │ ├── OnSenderListener.java │ │ │ │ ├── Sender.java │ │ │ │ ├── sendqueue │ │ │ │ ├── ISendQueue.java │ │ │ │ ├── NormalSendQueue.java │ │ │ │ └── SendQueueListener.java │ │ │ │ ├── tcp │ │ │ │ ├── EncodeV1.java │ │ │ │ ├── TcpConnection.java │ │ │ │ ├── TcpReadThread.java │ │ │ │ ├── TcpSender.java │ │ │ │ ├── TcpWriteThread.java │ │ │ │ └── interf │ │ │ │ │ ├── OnTcpReadListener.java │ │ │ │ │ ├── OnTcpWriteListener.java │ │ │ │ │ └── TcpConnectListener.java │ │ │ │ └── udp │ │ │ │ ├── UDPClientThread.java │ │ │ │ └── interf │ │ │ │ └── OnUdpConnectListener.java │ │ │ ├── ui │ │ │ └── ScreenImageActivity.java │ │ │ ├── utils │ │ │ ├── AnalyticDataUtils.java │ │ │ ├── AudioUtils.java │ │ │ ├── BatteryUtils.java │ │ │ ├── BlackListHelper.java │ │ │ ├── ByteUtil.java │ │ │ ├── DialogUtils.java │ │ │ ├── NetWorkUtils.java │ │ │ ├── PreferenceUtils.java │ │ │ ├── SopCastLog.java │ │ │ ├── SopCastUtils.java │ │ │ ├── StatusBarUtil.java │ │ │ ├── SupportMultipleScreensUtil.java │ │ │ ├── ToastUtils.java │ │ │ └── WeakHandler.java │ │ │ ├── video │ │ │ └── OnVideoEncodeListener.java │ │ │ └── widget │ │ │ ├── CustomDialog.java │ │ │ └── LoadingDialog.java │ └── res │ │ ├── drawable-nodpi │ │ ├── ic_client.png │ │ ├── ic_lamp_close.png │ │ ├── ic_lamp_open.png │ │ ├── ic_lamp_press.png │ │ ├── ic_loading.png │ │ ├── ic_network.png │ │ └── ic_server.png │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── bg_recommend.xml │ │ ├── dialog_bg.xml │ │ ├── f_new_image_progress_popbox.xml │ │ ├── ic_launcher_background.xml │ │ ├── scan_image.png │ │ └── select_lamp_icon.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_scan_qr_code.xml │ │ ├── activity_screen_image.xml │ │ ├── custom_camera.xml │ │ ├── loadingdialog.xml │ │ └── view_alertdialog.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 │ └── test │ └── screenimage │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | #Android.gitignore 2 | # Built application files 3 | *.apk 4 | *.ap_ 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # JetBrains Rider 21 | .idea/ 22 | *.sln.iml 23 | 24 | # Log Files 25 | *.log 26 | 27 | # Android Studio Navigation editor temp files 28 | .navigation/ 29 | 30 | # Android Studio captures folder 31 | captures/ 32 | 33 | # IntelliJ 34 | *.iml 35 | .idea/workspace.xml 36 | .idea/tasks.xml 37 | .idea/gradle.xml 38 | .idea/dictionaries 39 | .idea/libraries 40 | 41 | # Local configuration file (sdk path, etc) 42 | local.properties 43 | 44 | -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtt123/ScreenImage/0fef35e3491004431639aadc5176a3d2341bf309/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtt123/ScreenImage/0fef35e3491004431639aadc5176a3d2341bf309/README -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScreenImage 2 | 一对多投屏采集端 3 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | defaultConfig { 6 | applicationId "com.test.screenimage" 7 | minSdkVersion 15 8 | targetSdkVersion 27 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | compileOptions { 14 | sourceCompatibility JavaVersion.VERSION_1_8 15 | targetCompatibility JavaVersion.VERSION_1_8 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | implementation 'com.android.support:appcompat-v7:27.1.1' 28 | implementation 'com.jakewharton:butterknife:8.8.1' 29 | implementation 'cn.yipianfengye.android:zxing-library:2.2' 30 | implementation 'com.android.support.constraint:constraint-layout:1.1.0' 31 | implementation 'io.reactivex.rxjava2:rxjava:2.1.0' 32 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' 33 | implementation 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar' 34 | implementation 'com.github.skydoves:elasticviews:2.0.0' 35 | implementation 'org.greenrobot:eventbus:3.1.1' 36 | testImplementation 'junit:junit:4.12' 37 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' 38 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 39 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 40 | } 41 | -------------------------------------------------------------------------------- /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/test/screenimage/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage; 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() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.kedacom.screenimage", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/MyApplication.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage; 2 | 3 | import android.app.Application; 4 | import android.os.Handler; 5 | 6 | import com.test.screenimage.utils.SupportMultipleScreensUtil; 7 | 8 | 9 | /** 10 | * Created by wt on 2018/5/28. 11 | */ 12 | 13 | public class MyApplication extends Application { 14 | public static Handler mHandler; 15 | 16 | @Override 17 | public void onCreate() { 18 | super.onCreate(); 19 | //让代码执行在主线程 20 | mHandler = new Handler(getMainLooper()); 21 | SupportMultipleScreensUtil.init(this); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/audio/AudioEncoder.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.audio; 2 | 3 | import android.annotation.TargetApi; 4 | import android.media.MediaCodec; 5 | 6 | 7 | import com.test.screenimage.configuration.AudioConfiguration; 8 | import com.test.screenimage.mediacodec.AudioMediaCodec; 9 | 10 | import java.nio.ByteBuffer; 11 | 12 | 13 | @TargetApi(18) 14 | public class AudioEncoder { 15 | private MediaCodec mMediaCodec; 16 | private OnAudioEncodeListener mListener; 17 | private AudioConfiguration mAudioConfiguration; 18 | MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo(); 19 | 20 | public void setOnAudioEncodeListener(OnAudioEncodeListener listener) { 21 | mListener = listener; 22 | } 23 | 24 | public AudioEncoder(AudioConfiguration audioConfiguration) { 25 | mAudioConfiguration = audioConfiguration; 26 | } 27 | 28 | void prepareEncoder() { 29 | mMediaCodec = AudioMediaCodec.getAudioMediaCodec(mAudioConfiguration); 30 | //开始编码 31 | mMediaCodec.start(); 32 | } 33 | 34 | synchronized public void stop() { 35 | if (mMediaCodec != null) { 36 | mMediaCodec.stop(); 37 | mMediaCodec.release(); 38 | mMediaCodec = null; 39 | } 40 | } 41 | 42 | synchronized void offerEncoder(byte[] input) { 43 | if(mMediaCodec == null) { 44 | return; 45 | } 46 | ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); 47 | ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers(); 48 | int inputBufferIndex = mMediaCodec.dequeueInputBuffer(12000); 49 | if (inputBufferIndex >= 0) { 50 | ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; 51 | inputBuffer.clear(); 52 | inputBuffer.put(input); 53 | mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, 0, 0); 54 | } 55 | 56 | int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 12000); 57 | while (outputBufferIndex >= 0) { 58 | ByteBuffer outputBuffer = outputBuffers[outputBufferIndex]; 59 | if(mListener != null) { 60 | mListener.onAudioEncode(outputBuffer, mBufferInfo); 61 | } 62 | mMediaCodec.releaseOutputBuffer(outputBufferIndex, false); 63 | outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 0); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/audio/AudioProcessor.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.audio; 2 | 3 | import android.media.AudioRecord; 4 | 5 | import com.test.screenimage.configuration.AudioConfiguration; 6 | import com.test.screenimage.utils.AudioUtils; 7 | 8 | 9 | import java.util.Arrays; 10 | 11 | 12 | public class AudioProcessor extends Thread { 13 | private volatile boolean mPauseFlag; 14 | private volatile boolean mStopFlag; 15 | private volatile boolean mMute; 16 | private AudioRecord mAudioRecord; 17 | private AudioEncoder mAudioEncoder; 18 | private byte[] mRecordBuffer; 19 | private int mRecordBufferSize; 20 | 21 | public AudioProcessor(AudioRecord audioRecord, AudioConfiguration audioConfiguration) { 22 | mRecordBufferSize = AudioUtils.getRecordBufferSize(audioConfiguration); 23 | mRecordBuffer = new byte[mRecordBufferSize]; 24 | mAudioRecord = audioRecord; 25 | mAudioEncoder = new AudioEncoder(audioConfiguration); 26 | mAudioEncoder.prepareEncoder(); 27 | } 28 | 29 | public void setAudioHEncodeListener(OnAudioEncodeListener listener) { 30 | mAudioEncoder.setOnAudioEncodeListener(listener); 31 | } 32 | 33 | public void stopEncode() { 34 | mStopFlag = true; 35 | if(mAudioEncoder != null) { 36 | mAudioEncoder.stop(); 37 | mAudioEncoder = null; 38 | } 39 | } 40 | 41 | public void pauseEncode(boolean pause) { 42 | mPauseFlag = pause; 43 | } 44 | 45 | public void setMute(boolean mute) { 46 | mMute = mute; 47 | } 48 | 49 | public void run() { 50 | while (!mStopFlag) { 51 | while (mPauseFlag) { 52 | try { 53 | Thread.sleep(1000); 54 | } catch (InterruptedException e) { 55 | e.printStackTrace(); 56 | } 57 | } 58 | int readLen = mAudioRecord.read(mRecordBuffer, 0, mRecordBufferSize); 59 | if (readLen > 0) { 60 | if (mMute) { 61 | byte clearM = 0; 62 | Arrays.fill(mRecordBuffer, clearM); 63 | } 64 | if(mAudioEncoder != null) { 65 | mAudioEncoder.offerEncoder(mRecordBuffer); 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/audio/OnAudioEncodeListener.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.audio; 2 | 3 | import android.media.MediaCodec; 4 | 5 | import java.nio.ByteBuffer; 6 | 7 | 8 | public interface OnAudioEncodeListener { 9 | void onAudioEncode(ByteBuffer bb, MediaCodec.BufferInfo bi); 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/configuration/AudioConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.configuration; 2 | 3 | import android.media.AudioFormat; 4 | import android.media.AudioRecord; 5 | import android.media.MediaCodecInfo; 6 | import android.util.Log; 7 | 8 | public final class AudioConfiguration { 9 | //采样率 10 | public static final int DEFAULT_FREQUENCY = 44100; 11 | // public static final int DEFAULT_FREQUENCY = 8000; 12 | public static final int DEFAULT_MAX_BPS = 64; 13 | public static final int DEFAULT_MIN_BPS = 32; 14 | public static final int DEFAULT_ADTS = 0; 15 | public static final String DEFAULT_MIME = "audio/mp4a-latm"; 16 | //采样位数(定义音频编码(16位)) 17 | //设置每个样本的分辨率为16位或者8位,16位将占用更多的空间和处理能力,表示的音频也更加接近真实 18 | public static final int DEFAULT_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT; 19 | public static final int DEFAULT_AAC_PROFILE = MediaCodecInfo.CodecProfileLevel.AACObjectLC; 20 | public static final int DEFAULT_CHANNEL_COUNT = 2; 21 | public static final boolean DEFAULT_AEC = true; 22 | 23 | public final int minBps; 24 | public final int maxBps; 25 | public final int frequency; 26 | public final int encoding; 27 | public final int channelCount; 28 | public final int adts; 29 | public final int aacProfile; 30 | public final String mime; 31 | public final boolean aec; 32 | 33 | private AudioConfiguration(final Builder builder) { 34 | minBps = builder.minBps; 35 | maxBps = builder.maxBps; 36 | frequency = builder.frequency; 37 | encoding = builder.encoding; 38 | channelCount = builder.channelCount; 39 | adts = builder.adts; 40 | mime = builder.mime; 41 | aacProfile = builder.aacProfile; 42 | aec = builder.aec; 43 | Log.e("wtt", "AudioConfiguration: "+frequency ); 44 | } 45 | 46 | public static AudioConfiguration createDefault() { 47 | return new Builder().build(); 48 | } 49 | 50 | public static class Builder { 51 | private int minBps = DEFAULT_MIN_BPS; 52 | private int maxBps = DEFAULT_MAX_BPS; 53 | private int frequency = DEFAULT_FREQUENCY; 54 | private int encoding = DEFAULT_AUDIO_ENCODING; 55 | private int channelCount = DEFAULT_CHANNEL_COUNT; 56 | private int adts = DEFAULT_ADTS; 57 | private String mime = DEFAULT_MIME; 58 | private int aacProfile = DEFAULT_AAC_PROFILE; 59 | private boolean aec = DEFAULT_AEC; 60 | 61 | public Builder setBps(int minBps, int maxBps) { 62 | this.minBps = minBps; 63 | this.maxBps = maxBps; 64 | return this; 65 | } 66 | 67 | public Builder setFrequency(int frequency) { 68 | this.frequency = frequency; 69 | return this; 70 | } 71 | 72 | public Builder setEncoding(int encoding) { 73 | this.encoding = encoding; 74 | return this; 75 | } 76 | 77 | public Builder setChannelCount(int channelCount) { 78 | this.channelCount = channelCount; 79 | return this; 80 | } 81 | 82 | public Builder setAdts(int adts) { 83 | this.adts = adts; 84 | return this; 85 | } 86 | 87 | public Builder setAacProfile(int aacProfile) { 88 | this.aacProfile = aacProfile; 89 | return this; 90 | } 91 | 92 | public Builder setMime(String mime) { 93 | this.mime = mime; 94 | return this; 95 | } 96 | 97 | public Builder setAec(boolean aec) { 98 | this.aec = aec; 99 | return this; 100 | } 101 | 102 | public AudioConfiguration build() { 103 | return new AudioConfiguration(this); 104 | } 105 | 106 | } 107 | 108 | 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/configuration/VideoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.configuration; 2 | 3 | 4 | public final class VideoConfiguration { 5 | // TODO modify by xu.wang 6 | //--------------------默认标清时的设置参数-------------------------------- 7 | public static final int DEFAULT_HEIGHT = 640; 8 | public static final int DEFAULT_WIDTH = 360; 9 | public static final int DEFAULT_FPS = 24; 10 | public static final int DEFAULT_MAX_BPS = 1500; 11 | public static final int DEFAULT_MIN_BPS = 400; 12 | public static final int DEFAULT_IFI = 1; //关键帧间隔时间 单位s 13 | public static final String DEFAULT_MIME = "video/avc"; 14 | // //--------------------默认标清时的设置参数-------------------------------- 15 | // public static final int SECOND_HEIGHT = 640; 16 | // public static final int SECOND_WIDTH = 360; 17 | // public static final int SECOND_FPS = 10; 18 | // public static final int SECOND_MAX_BPS = 600; 19 | // public static final int SECOND_MIN_BPS = 300; 20 | // public static final int SECOND_IFI = 3; 21 | // public static final String SECOND_MIME = "video/avc"; 22 | 23 | public int height; 24 | public int width; 25 | public final int minBps; 26 | public int maxBps; 27 | public int fps; 28 | public int ifi; 29 | public final String mime; 30 | 31 | private VideoConfiguration(final Builder builder) { 32 | height = builder.height; 33 | width = builder.width; 34 | minBps = builder.minBps; 35 | maxBps = builder.maxBps; 36 | fps = builder.fps; 37 | ifi = builder.ifi; 38 | mime = builder.mime; 39 | } 40 | 41 | // private VideoConfiguration(final SecondBuilder builder) { 42 | // height = builder.height; 43 | // width = builder.width; 44 | // minBps = builder.minBps; 45 | // maxBps = builder.maxBps; 46 | // fps = builder.fps; 47 | // ifi = builder.ifi; 48 | // mime = builder.mime; 49 | // } 50 | 51 | public static VideoConfiguration createDefault() { 52 | return new Builder().build(); 53 | } 54 | 55 | 56 | public static class Builder { 57 | private int height = DEFAULT_HEIGHT; 58 | private int width = DEFAULT_WIDTH; 59 | private int minBps = DEFAULT_MIN_BPS; 60 | private int maxBps = DEFAULT_MAX_BPS; 61 | private int fps = DEFAULT_FPS; 62 | private int ifi = DEFAULT_IFI; 63 | private String mime = DEFAULT_MIME; 64 | 65 | public Builder setSize(int width, int height) { 66 | this.width = width; 67 | this.height = height; 68 | return this; 69 | } 70 | 71 | public Builder setBps(int minBps, int maxBps) { 72 | this.minBps = minBps; 73 | this.maxBps = maxBps; 74 | return this; 75 | } 76 | 77 | public Builder setFps(int fps) { 78 | this.fps = fps; 79 | return this; 80 | } 81 | 82 | public Builder setIfi(int ifi) { 83 | this.ifi = ifi; 84 | return this; 85 | } 86 | 87 | public Builder setMime(String mime) { 88 | this.mime = mime; 89 | return this; 90 | } 91 | 92 | public VideoConfiguration build() { 93 | return new VideoConfiguration(this); 94 | } 95 | } 96 | // 97 | // public static class SecondBuilder { 98 | // private int height = SECOND_HEIGHT; 99 | // private int width = SECOND_WIDTH; 100 | // private int minBps = SECOND_MIN_BPS; 101 | // private int maxBps = SECOND_MAX_BPS; 102 | // private int fps = SECOND_FPS; 103 | // private int ifi = SECOND_IFI; 104 | // private String mime = SECOND_MIME; 105 | // 106 | // public VideoConfiguration build() { 107 | // return new VideoConfiguration(this); 108 | // } 109 | // } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/constant/Constants.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.constant; 2 | 3 | /** 4 | * Created by wt on 2018/5/28. 5 | */ 6 | public class Constants { 7 | public static final String TAG = "wtt"; 8 | public static String PHONEIP = "phoneIp"; 9 | public static String PCIP = "pcIp"; 10 | public static long MASK = 0; 11 | public static int WIDTH=360; 12 | public static int HEIGHT=640; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/constant/ScreenImageApi.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.constant; 2 | 3 | /** 4 | * Created by wt on 2018/6/11. 5 | * 设备常用配置 6 | */ 7 | public class ScreenImageApi { 8 | 9 | public static final byte encodeVersion1 = 0x00; //版本号1 10 | 11 | public class RECORD { //录屏指令 12 | public static final int MAIN_CMD = 0xA2; //录屏主指令 13 | public static final short RECORDER_REQUEST_START = 0x01; //投屏,请求开始 14 | public static final int Command_HostUpdated = 0x1C; //{IP}:{Name}|{IP}:{Name}…投屏者信息 15 | public static final int DATE_PALY = 0x01;//音视频解析播放 16 | } 17 | 18 | public class SERVER {//服务端与客户端交互指令 19 | public static final int MAIN_CMD = 0xA0; //投屏回传主指令 20 | public static final int INITIAL_SUCCESS = 0x01;//服务器初始化成功 21 | } 22 | 23 | public class LOGIC_REQUEST { //客户端向服务器请求的指令 24 | public static final int MAIN_CMD = 112; 25 | public static final int GET_START_INFO = 1; //传输开始投屏需要的关键信息 例 1920,1080(长,宽) 26 | } 27 | 28 | public class LOGIC_REPONSE { //返回给客户端的指令 29 | public static final int MAIN_CMD = 111; 30 | 31 | public static final int GET_START_INFO = 1; //传输开始投屏需要的关键信息 例 1920,1080(长,宽) 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/controller/StreamController.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.controller; 2 | 3 | import android.media.MediaCodec; 4 | 5 | import com.test.screenimage.audio.OnAudioEncodeListener; 6 | import com.test.screenimage.configuration.AudioConfiguration; 7 | import com.test.screenimage.configuration.VideoConfiguration; 8 | import com.test.screenimage.controller.audio.IAudioController; 9 | import com.test.screenimage.controller.video.IVideoController; 10 | import com.test.screenimage.stream.packer.Packer; 11 | import com.test.screenimage.stream.sender.Sender; 12 | import com.test.screenimage.stream.sender.tcp.TcpSender; 13 | import com.test.screenimage.utils.SopCastUtils; 14 | import com.test.screenimage.video.OnVideoEncodeListener; 15 | 16 | import java.nio.ByteBuffer; 17 | 18 | public class StreamController implements OnAudioEncodeListener, OnVideoEncodeListener, Packer.OnPacketListener { 19 | private Packer mPacker; 20 | private Sender mSender; 21 | private IVideoController mVideoController; 22 | private IAudioController mAudioController; 23 | 24 | public StreamController(IVideoController videoProcessor, IAudioController audioProcessor) { 25 | mAudioController = audioProcessor; 26 | mVideoController = videoProcessor; 27 | } 28 | 29 | public void setVideoConfiguration(VideoConfiguration videoConfiguration) { 30 | mVideoController.setVideoConfiguration(videoConfiguration); 31 | } 32 | 33 | 34 | public void setPacker(Packer packer) { 35 | mPacker = packer; 36 | //设置打包监听器 37 | mPacker.setPacketListener(this); 38 | } 39 | 40 | public void setSender(Sender sender) { 41 | mSender = sender; 42 | } 43 | 44 | public synchronized void start() { 45 | SopCastUtils.processNotUI(new SopCastUtils.INotUIProcessor() { 46 | @Override 47 | public void process() { 48 | if (mPacker == null) { 49 | return; 50 | } 51 | if (mSender == null) { 52 | return; 53 | } 54 | //开始打包 55 | mPacker.start(); 56 | //开始发送 57 | mSender.start(); 58 | mVideoController.setVideoEncoderListener(StreamController.this); 59 | mVideoController.start(); 60 | if (mAudioController != null) { 61 | mAudioController.setAudioEncodeListener(StreamController.this); 62 | mAudioController.start(); 63 | } 64 | } 65 | }); 66 | } 67 | 68 | public synchronized void stop() { 69 | SopCastUtils.processNotUI(new SopCastUtils.INotUIProcessor() { 70 | @Override 71 | public void process() { 72 | if (mAudioController != null) { 73 | mAudioController.setAudioEncodeListener(null); 74 | mAudioController.stop(); 75 | } 76 | if (mVideoController != null) { 77 | mVideoController.setVideoEncoderListener(null); 78 | mVideoController.stop(); 79 | } 80 | if (mSender != null) { 81 | mSender.stop(); 82 | } 83 | if (mPacker != null) { 84 | mPacker.stop(); 85 | } 86 | } 87 | }); 88 | } 89 | 90 | public synchronized void pause() { 91 | SopCastUtils.processNotUI(new SopCastUtils.INotUIProcessor() { 92 | @Override 93 | public void process() { 94 | if (mAudioController != null) mAudioController.pause(); 95 | mVideoController.pause(); 96 | } 97 | }); 98 | } 99 | 100 | public synchronized void resume() { 101 | SopCastUtils.processNotUI(new SopCastUtils.INotUIProcessor() { 102 | @Override 103 | public void process() { 104 | if (mAudioController != null) mAudioController.resume(); 105 | mVideoController.resume(); 106 | } 107 | }); 108 | } 109 | 110 | public void mute(boolean mute) { 111 | if (mAudioController != null) mAudioController.mute(mute); 112 | } 113 | 114 | public int getSessionId() { 115 | return mAudioController == null ? 0 : mAudioController.getSessionId(); 116 | } 117 | 118 | public boolean setVideoBps(int bps) { 119 | return mVideoController.setVideoBps(bps); 120 | } 121 | 122 | @Override 123 | public void onAudioEncode(ByteBuffer bb, MediaCodec.BufferInfo bi) { 124 | if (mPacker != null) { 125 | mPacker.onAudioData(bb, bi); 126 | } 127 | } 128 | 129 | @Override 130 | public void onVideoEncode(ByteBuffer bb, MediaCodec.BufferInfo bi) { 131 | if (mPacker != null) { 132 | mPacker.onVideoData(bb, bi); 133 | } 134 | } 135 | 136 | @Override 137 | public void onPacket(byte[] data, int packetType) { 138 | if (mSender != null) { 139 | mSender.onData(data, packetType); 140 | } 141 | } 142 | 143 | @Override 144 | public void onSpsPps(byte[] mSpsPps) { 145 | if (mSender != null && mSender instanceof TcpSender) { 146 | ((TcpSender) mSender).setSpsPps(mSpsPps); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/controller/audio/IAudioController.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.controller.audio; 2 | 3 | import com.test.screenimage.audio.OnAudioEncodeListener; 4 | import com.test.screenimage.configuration.AudioConfiguration; 5 | 6 | public interface IAudioController { 7 | void start(); 8 | void stop(); 9 | void pause(); 10 | void resume(); 11 | void mute(boolean mute); 12 | int getSessionId(); 13 | void setAudioEncodeListener(OnAudioEncodeListener listener); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/controller/audio/NormalAudioController.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.controller.audio; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.media.AudioManager; 6 | import android.media.AudioRecord; 7 | import android.util.Log; 8 | 9 | import com.test.screenimage.audio.AudioProcessor; 10 | import com.test.screenimage.audio.OnAudioEncodeListener; 11 | import com.test.screenimage.configuration.AudioConfiguration; 12 | import com.test.screenimage.constant.Constants; 13 | import com.test.screenimage.utils.AudioUtils; 14 | import com.test.screenimage.utils.SopCastLog; 15 | 16 | public class NormalAudioController implements IAudioController { 17 | private OnAudioEncodeListener mListener; 18 | private AudioRecord mAudioRecord; 19 | private AudioProcessor mAudioProcessor; 20 | // TODO: 2018/6/5 增加上下文 21 | private Context mContext; 22 | private boolean mMute; 23 | private AudioConfiguration mAudioConfiguration; 24 | 25 | public NormalAudioController(Context context) { 26 | this.mContext=context; 27 | mAudioConfiguration = AudioConfiguration.createDefault(); 28 | } 29 | 30 | public void setAudioConfiguration(AudioConfiguration audioConfiguration) { 31 | mAudioConfiguration = audioConfiguration; 32 | } 33 | 34 | public void setAudioEncodeListener(OnAudioEncodeListener listener) { 35 | mListener = listener; 36 | } 37 | 38 | public void start() { 39 | SopCastLog.d(Constants.TAG, "Audio Recording start"); 40 | mAudioRecord = AudioUtils.getAudioRecord(mAudioConfiguration,mContext); 41 | try { 42 | //音频录制 43 | mAudioRecord.startRecording(); 44 | } catch (Exception e) { 45 | e.printStackTrace(); 46 | } 47 | mAudioProcessor = new AudioProcessor(mAudioRecord, mAudioConfiguration); 48 | mAudioProcessor.setAudioHEncodeListener(mListener); 49 | mAudioProcessor.start(); 50 | mAudioProcessor.setMute(mMute); 51 | } 52 | 53 | public void stop() { 54 | SopCastLog.d(Constants.TAG, "Audio Recording stop"); 55 | if(mAudioProcessor != null) { 56 | mAudioProcessor.stopEncode(); 57 | } 58 | if(mAudioRecord != null) { 59 | try { 60 | mAudioRecord.stop(); 61 | mAudioRecord.release(); 62 | mAudioRecord = null; 63 | } catch (Exception e) { 64 | e.printStackTrace(); 65 | } 66 | } 67 | } 68 | 69 | public void pause() { 70 | SopCastLog.d(Constants.TAG, "Audio Recording pause"); 71 | try { 72 | if (mAudioRecord != null) { 73 | mAudioRecord.stop(); 74 | } 75 | if (mAudioProcessor != null) { 76 | mAudioProcessor.pauseEncode(true); 77 | } 78 | }catch (Exception e){ 79 | Log.e("NormalAudioController","" + e.toString()); 80 | } 81 | } 82 | 83 | public void resume() { 84 | SopCastLog.d(Constants.TAG, "Audio Recording resume"); 85 | if(mAudioRecord != null) { 86 | mAudioRecord.startRecording(); 87 | } 88 | if (mAudioProcessor != null) { 89 | mAudioProcessor.pauseEncode(false); 90 | } 91 | } 92 | 93 | public void mute(boolean mute) { 94 | SopCastLog.d(Constants.TAG, "Audio Recording mute: " + mute); 95 | mMute = mute; 96 | if(mAudioProcessor != null) { 97 | mAudioProcessor.setMute(mMute); 98 | } 99 | } 100 | 101 | @Override 102 | @TargetApi(16) 103 | public int getSessionId() { 104 | if(mAudioRecord != null) { 105 | return mAudioRecord.getAudioSessionId(); 106 | } else { 107 | return -1; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/controller/video/IVideoController.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.controller.video; 2 | 3 | import com.test.screenimage.configuration.VideoConfiguration; 4 | import com.test.screenimage.video.OnVideoEncodeListener; 5 | 6 | public interface IVideoController { 7 | void start(); 8 | void stop(); 9 | void pause(); 10 | void resume(); 11 | boolean setVideoBps(int bps); 12 | void setVideoEncoderListener(OnVideoEncodeListener listener); 13 | void setVideoConfiguration(VideoConfiguration configuration); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/controller/video/ScreenVideoController.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.controller.video; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Intent; 5 | import android.hardware.display.DisplayManager; 6 | import android.hardware.display.VirtualDisplay; 7 | import android.media.projection.MediaProjection; 8 | import android.media.projection.MediaProjectionManager; 9 | import android.os.Build; 10 | import android.view.Surface; 11 | 12 | import com.test.screenimage.configuration.VideoConfiguration; 13 | import com.test.screenimage.constant.Constants; 14 | import com.test.screenimage.mediacodec.VideoMediaCodec; 15 | import com.test.screenimage.screen.ScreenRecordEncoder; 16 | import com.test.screenimage.utils.SopCastLog; 17 | import com.test.screenimage.video.OnVideoEncodeListener; 18 | 19 | 20 | import static android.os.Build.VERSION_CODES.LOLLIPOP; 21 | 22 | 23 | @TargetApi(LOLLIPOP) 24 | public class ScreenVideoController implements IVideoController { 25 | private MediaProjectionManager mManager; 26 | private int resultCode; 27 | private Intent resultData; 28 | private VirtualDisplay mVirtualDisplay; 29 | private MediaProjection mMediaProjection; 30 | private VideoConfiguration mVideoConfiguration = VideoConfiguration.createDefault(); 31 | private ScreenRecordEncoder mEncoder; 32 | private OnVideoEncodeListener mListener; 33 | 34 | public ScreenVideoController(MediaProjectionManager manager, int resultCode, Intent resultData) { 35 | mManager = manager; 36 | this.resultCode = resultCode; 37 | this.resultData = resultData; 38 | } 39 | 40 | @Override 41 | public void start() { 42 | //关于屏幕采集 43 | mEncoder = new ScreenRecordEncoder(mVideoConfiguration); 44 | final Surface surface = mEncoder.getSurface(); 45 | mEncoder.start(); 46 | mEncoder.setOnVideoEncodeListener(mListener); 47 | mMediaProjection = mManager.getMediaProjection(resultCode, resultData); 48 | int width = VideoMediaCodec.getVideoSize(mVideoConfiguration.width); 49 | int height = VideoMediaCodec.getVideoSize(mVideoConfiguration.height); 50 | //实例化VirtualDisplay,这个类的主要作用是用来获取屏幕信息并保存在里。 51 | mVirtualDisplay = mMediaProjection.createVirtualDisplay("ScreenRecoder", 52 | width, height, 1, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, 53 | null, null); 54 | } 55 | 56 | @Override 57 | public void stop() { 58 | if (mEncoder != null) { 59 | mEncoder.setOnVideoEncodeListener(null); 60 | mEncoder.stop(); 61 | mEncoder = null; 62 | } 63 | if (mMediaProjection != null) { 64 | mMediaProjection.stop(); 65 | mMediaProjection = null; 66 | } 67 | if (mVirtualDisplay != null) { 68 | mVirtualDisplay.release(); 69 | mVirtualDisplay = null; 70 | } 71 | } 72 | 73 | @Override 74 | public void pause() { 75 | if (mEncoder != null) { 76 | mEncoder.setPause(true); 77 | } 78 | } 79 | 80 | @Override 81 | public void resume() { 82 | if (mEncoder != null) { 83 | mEncoder.setPause(false); 84 | } 85 | } 86 | 87 | @Override 88 | public boolean setVideoBps(int bps) { 89 | //重新设置硬编bps,在低于19的版本需要重启编码器 90 | boolean result = false; 91 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { 92 | //由于重启硬编编码器效果不好,此次不做处理 93 | SopCastLog.d(Constants.TAG, "Bps need change, but MediaCodec do not support."); 94 | } else { 95 | if (mEncoder != null) { 96 | SopCastLog.d(Constants.TAG, "Bps change, current bps: " + bps); 97 | mEncoder.setRecorderBps(bps); 98 | result = true; 99 | } 100 | } 101 | return result; 102 | } 103 | 104 | @Override 105 | public void setVideoEncoderListener(OnVideoEncodeListener listener) { 106 | mListener = listener; 107 | } 108 | 109 | @Override 110 | public void setVideoConfiguration(VideoConfiguration configuration) { 111 | mVideoConfiguration = configuration; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/core/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.core; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.View; 7 | 8 | import com.test.screenimage.utils.StatusBarUtil; 9 | import com.test.screenimage.utils.SupportMultipleScreensUtil; 10 | import com.test.screenimage.utils.ToastUtils; 11 | 12 | import butterknife.ButterKnife; 13 | 14 | 15 | /** 16 | * Created by wt on 2018/5/28. 17 | */ 18 | public abstract class BaseActivity extends AppCompatActivity implements View.OnClickListener { 19 | private static long lastTimeStamp = 0l; 20 | 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | if (getLayoutId() != 0) { 26 | StatusBarUtil.transparencyBar(this); 27 | StatusBarUtil.StatusBarLightMode(this); 28 | setContentView(getLayoutId()); 29 | View rootView = findViewById(android.R.id.content); 30 | SupportMultipleScreensUtil.scale(rootView); 31 | ButterKnife.bind(this); 32 | } 33 | initView(); 34 | initData(); 35 | } 36 | 37 | protected abstract void initView(); 38 | 39 | protected abstract void initData(); 40 | 41 | protected int getLayoutId() { 42 | return 0; 43 | } 44 | 45 | 46 | @Override 47 | protected void onDestroy() { 48 | super.onDestroy(); 49 | } 50 | 51 | @Override 52 | public void onClick(View v) { 53 | 54 | } 55 | 56 | /** 57 | * 退出程序. 58 | * 59 | * @param context 60 | */ 61 | public static void exitApplication(Activity context) { 62 | long currentTimeStamp = System.currentTimeMillis(); 63 | if (currentTimeStamp - lastTimeStamp > 1350L) { 64 | ToastUtils.showToast(context, "再按一次退出"); 65 | } else { 66 | context.finish(); 67 | } 68 | lastTimeStamp = currentTimeStamp; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/entity/Frame.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.entity; 2 | 3 | // TODO: 2018/5/29 wt修改Frame 4 | public class Frame { 5 | public static final int FRAME_TYPE_AUDIO = 1; 6 | public static final int FRAME_TYPE_KEY_FRAME = 2; 7 | public static final int FRAME_TYPE_INTER_FRAME = 3; 8 | public static final int FRAME_TYPE_CONFIGURATION = 4; 9 | 10 | // public T data; 11 | public byte[] data; 12 | public int packetType; 13 | public int frameType; 14 | 15 | public Frame(byte[] data, int packetType, int frameType) { 16 | this.data = data; 17 | this.packetType = packetType; 18 | this.frameType = frameType; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/entity/ReceiveData.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.entity; 2 | 3 | /** 4 | * Created by wt on 2018/6/12. 5 | * 解析后的一组数据 6 | */ 7 | public class ReceiveData { 8 | private ReceiveHeader header; 9 | private String sendBody; 10 | private byte[] buff; 11 | 12 | public ReceiveData() { 13 | } 14 | 15 | /** 16 | * @param header 协议头 17 | * @param sendBody 文本内容 18 | * @param buff 数组内容 19 | */ 20 | public ReceiveData(ReceiveHeader header, String sendBody, byte[] buff) { 21 | this.header = header; 22 | this.sendBody = sendBody; 23 | this.buff = buff; 24 | } 25 | 26 | public ReceiveHeader getHeader() { 27 | return header; 28 | } 29 | 30 | public void setHeader(ReceiveHeader header) { 31 | this.header = header; 32 | } 33 | 34 | public String getSendBody() { 35 | return sendBody; 36 | } 37 | 38 | public void setSendBody(String sendBody) { 39 | this.sendBody = sendBody; 40 | } 41 | 42 | public byte[] getBuff() { 43 | return buff; 44 | } 45 | 46 | public void setBuff(byte[] buff) { 47 | this.buff = buff; 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/entity/ReceiveHeader.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.entity; 2 | 3 | /** 4 | * Created by wt on 2018/6/12. 5 | * 解析出协议数据头格式 6 | * mainCmd 主指令 7 | * subCmd 子指令 8 | * encodeVersion 编码版本 9 | * stringBodylength 文本消息内容长度 10 | * buffSize 音视频内容长度 11 | */ 12 | public class ReceiveHeader { 13 | private int mainCmd; 14 | private int subCmd; 15 | private byte encodeVersion; 16 | private int stringBodylength; 17 | private int buffSize; 18 | 19 | public ReceiveHeader(int mainCmd, int subCmd, byte encodeVersion, int stringBodylength, int buffSize) { 20 | this.mainCmd = mainCmd; 21 | this.subCmd = subCmd; 22 | this.encodeVersion = encodeVersion; 23 | this.stringBodylength = stringBodylength; 24 | this.buffSize = buffSize; 25 | } 26 | 27 | public int getMainCmd() { 28 | return mainCmd; 29 | } 30 | 31 | public void setMainCmd(int mainCmd) { 32 | this.mainCmd = mainCmd; 33 | } 34 | 35 | public int getSubCmd() { 36 | return subCmd; 37 | } 38 | 39 | public void setSubCmd(int subCmd) { 40 | this.subCmd = subCmd; 41 | } 42 | 43 | public byte getEncodeVersion() { 44 | return encodeVersion; 45 | } 46 | 47 | public void setEncodeVersion(byte encodeVersion) { 48 | this.encodeVersion = encodeVersion; 49 | } 50 | 51 | public int getStringBodylength() { 52 | return stringBodylength; 53 | } 54 | 55 | public void setStringBodylength(int stringBodylength) { 56 | this.stringBodylength = stringBodylength; 57 | } 58 | 59 | public int getBuffSize() { 60 | return buffSize; 61 | } 62 | 63 | public void setBuffSize(int buffSize) { 64 | this.buffSize = buffSize; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/mediacodec/AudioMediaCodec.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.mediacodec; 2 | 3 | import android.annotation.TargetApi; 4 | import android.media.MediaCodec; 5 | import android.media.MediaFormat; 6 | 7 | import com.test.screenimage.configuration.AudioConfiguration; 8 | import com.test.screenimage.utils.AudioUtils; 9 | 10 | 11 | @TargetApi(18) 12 | public class AudioMediaCodec { 13 | //pcm转成aac 14 | public static MediaCodec getAudioMediaCodec(AudioConfiguration configuration) { 15 | //设置音频编码格式,获取编码器实例 16 | //初始化 此格式使用的音频编码技术、音频采样率、使用此格式的音频信道数(单声道为 1,立体声为 2) 17 | MediaFormat format = MediaFormat.createAudioFormat(configuration.mime, 18 | configuration.frequency, configuration.channelCount); 19 | if (configuration.mime.equals(AudioConfiguration.DEFAULT_MIME)) { 20 | format.setInteger(MediaFormat.KEY_AAC_PROFILE, configuration.aacProfile); 21 | } 22 | //比特率 声音中的比特率是指将模拟声音信号转换成数字声音信号后,单位时间内的二进制数据量 23 | // 是间接衡量音频质量的一个指标 24 | format.setInteger(MediaFormat.KEY_BIT_RATE, configuration.maxBps * 1024); 25 | //设置采样率 26 | format.setInteger(MediaFormat.KEY_SAMPLE_RATE, configuration.frequency); 27 | int maxInputSize = AudioUtils.getRecordBufferSize(configuration); 28 | //传入的数据大小 29 | format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize); 30 | format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, configuration.channelCount); 31 | 32 | MediaCodec mediaCodec = null; 33 | try { 34 | mediaCodec = MediaCodec.createEncoderByType(configuration.mime); 35 | //设置相关参数 36 | mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 37 | } catch (Exception e) { 38 | e.printStackTrace(); 39 | if (mediaCodec != null) { 40 | mediaCodec.stop(); 41 | mediaCodec.release(); 42 | mediaCodec = null; 43 | } 44 | } 45 | return mediaCodec; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/mediacodec/VideoMediaCodec.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.mediacodec; 2 | 3 | import android.annotation.TargetApi; 4 | import android.media.MediaCodec; 5 | import android.media.MediaCodecInfo; 6 | import android.media.MediaFormat; 7 | import android.os.Build; 8 | import android.util.Log; 9 | 10 | import com.test.screenimage.configuration.VideoConfiguration; 11 | import com.test.screenimage.constant.Constants; 12 | import com.test.screenimage.utils.BlackListHelper; 13 | import com.test.screenimage.utils.SopCastLog; 14 | 15 | 16 | @TargetApi(18) 17 | public class VideoMediaCodec { 18 | 19 | public static MediaCodec getVideoMediaCodec(VideoConfiguration videoConfiguration) { 20 | int videoWidth = getVideoSize(videoConfiguration.width); 21 | int videoHeight = getVideoSize(videoConfiguration.height); 22 | // if (Build.MANUFACTURER.equalsIgnoreCase("XIAOMI")) { 23 | // videoConfiguration.maxBps = 1500; 24 | // videoConfiguration.fps = 10; 25 | // videoConfiguration.ifi = 3; 26 | // } 27 | MediaFormat format = MediaFormat.createVideoFormat(videoConfiguration.mime, videoWidth, videoHeight); 28 | //设置颜色格式 29 | format.setInteger(MediaFormat.KEY_COLOR_FORMAT, 30 | MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); 31 | //设置比特率(设置码率,通常码率越高,视频越清晰) 32 | format.setInteger(MediaFormat.KEY_BIT_RATE, videoConfiguration.maxBps * 1024); 33 | int fps = videoConfiguration.fps; 34 | //设置摄像头预览帧率 35 | if (BlackListHelper.deviceInFpsBlacklisted()) { 36 | SopCastLog.d(Constants.TAG, "Device in fps setting black list, so set mediacodec fps 15"); 37 | fps = 15; 38 | } 39 | //设置帧率 40 | format.setInteger(MediaFormat.KEY_FRAME_RATE, fps); 41 | //关键帧间隔时间,通常情况下,你设置成多少问题都不大。 42 | //比如你设置成10,那就是10秒一个关键帧。但是,如果你有需求要做视频的预览,那你最好设置成1 43 | //因为如果你设置成10,那你会发现,10秒内的预览都是一个截图 44 | format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, videoConfiguration.ifi); 45 | // -----------------ADD BY XU.WANG 当画面静止时,重复最后一帧-------------------------------------------------------- 46 | format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000000 / 45); 47 | //------------------MODIFY BY XU.WANG 为解决MIUI9.5花屏而增加...------------------------------- 48 | if (Build.MANUFACTURER.equalsIgnoreCase("XIAOMI")) { 49 | //设置编码模式 50 | format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ); 51 | } else { 52 | format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR); 53 | } 54 | //设置复用模式 55 | format.setInteger(MediaFormat.KEY_COMPLEXITY, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR); 56 | MediaCodec mediaCodec = null; 57 | 58 | try { 59 | mediaCodec = MediaCodec.createEncoderByType(videoConfiguration.mime); 60 | mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 61 | } catch (Exception e) { 62 | e.printStackTrace(); 63 | if (mediaCodec != null) { 64 | mediaCodec.stop(); 65 | mediaCodec.release(); 66 | mediaCodec = null; 67 | } 68 | } 69 | return mediaCodec; 70 | } 71 | 72 | // We avoid the device-specific limitations on width and height by using values that 73 | // are multiples of 16, which all tested devices seem to be able to handle. 74 | public static int getVideoSize(int size) { 75 | int multiple = (int) Math.ceil(size / 16.0); 76 | return multiple * 16; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/net/OnTcpSendMessageListner.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.net; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Created by wt 7 | * Date on 2018/7/4 8 | */ 9 | public interface OnTcpSendMessageListner { 10 | void success(int mainCmd, int subCmd, String body, byte[] bytes); 11 | void error(Exception e); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/net/RequestTcp.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.net; 2 | 3 | import android.util.Log; 4 | 5 | import com.test.screenimage.MyApplication; 6 | import com.test.screenimage.entity.ReceiveData; 7 | import com.test.screenimage.entity.ReceiveHeader; 8 | import com.test.screenimage.stream.sender.tcp.EncodeV1; 9 | import com.test.screenimage.utils.AnalyticDataUtils; 10 | 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.OutputStream; 14 | import java.net.InetSocketAddress; 15 | import java.net.Socket; 16 | import java.net.SocketAddress; 17 | import java.net.SocketException; 18 | 19 | /** 20 | * Created by wt 21 | * Date on 2018/7/4 22 | * 23 | * @Desc 24 | */ 25 | 26 | public class RequestTcp extends Thread { 27 | private int mainCmd; 28 | private int subCmd; 29 | private byte[] bytes; 30 | private String ip; 31 | private int port; 32 | private String sendBody; 33 | private Socket mSocket; 34 | private AnalyticDataUtils mAnalyticDataUtils; 35 | private OnTcpSendMessageListner mListener; 36 | private int connectSoTime; 37 | private OutputStream outputStream; 38 | private InputStream inputStream; 39 | 40 | public RequestTcp(String ip, int port, int mainCmd, int subCmd, String sendBody, byte[] bytes, 41 | int connectSoTime, OnTcpSendMessageListner listener) { 42 | this.ip = ip; 43 | this.port = port; 44 | this.mainCmd = mainCmd; 45 | this.subCmd = subCmd; 46 | this.sendBody = sendBody; 47 | this.mListener = listener; 48 | this.bytes = bytes; 49 | this.connectSoTime = connectSoTime; 50 | } 51 | 52 | @Override 53 | public void run() { 54 | try { 55 | initialSendMessage(mainCmd, subCmd, sendBody, bytes, connectSoTime); 56 | } catch (final Exception e) { 57 | Log.e("wtt", e.toString()); 58 | if (MyApplication.mHandler != null) { 59 | MyApplication.mHandler.post(new Runnable() { 60 | @Override 61 | public void run() { 62 | if (mListener != null) { 63 | mListener.error(e); 64 | } 65 | } 66 | }); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * 发送消息 73 | * 74 | * @param mainCmd 主命令 75 | * @param subCmd 子命令 76 | * @param sendBody 发送消息 77 | * @param connectSoTime 超时时间 78 | * @throws Exception throws Exception 79 | */ 80 | private void initialSendMessage(int mainCmd, int subCmd, String sendBody, 81 | byte[] bytes, int connectSoTime) throws Exception { 82 | if (mAnalyticDataUtils == null) { 83 | mAnalyticDataUtils = new AnalyticDataUtils(); 84 | } 85 | mSocket = new Socket(); 86 | mSocket.setReuseAddress(true); 87 | SocketAddress socketAddress = new InetSocketAddress(ip, port); 88 | mSocket.connect(socketAddress, connectSoTime); 89 | mSocket.setSoTimeout(60000); //此方法意为tcp连接成功后is.read阻塞多长时间 90 | outputStream = mSocket.getOutputStream(); 91 | EncodeV1 encodeV1 = new EncodeV1(mainCmd, subCmd, sendBody, bytes); 92 | outputStream.write(encodeV1.buildSendContent()); 93 | outputStream.flush(); 94 | inputStream = mSocket.getInputStream(); 95 | byte[] tempBytes = mAnalyticDataUtils.readByte(inputStream, 18); 96 | ReceiveHeader receiveHeader = mAnalyticDataUtils.analysisHeader(tempBytes); 97 | ReceiveData receiveData = mAnalyticDataUtils.synchAnalyticData(inputStream, receiveHeader); 98 | if (mListener != null) { 99 | MyApplication.mHandler.post(new Runnable() { 100 | @Override 101 | public void run() { 102 | mListener.success(receiveData.getHeader().getMainCmd(), receiveData.getHeader().getSubCmd(), 103 | receiveData.getSendBody(), receiveData.getBuff()); 104 | } 105 | }); 106 | } 107 | mSocket.close(); 108 | 109 | 110 | } 111 | 112 | // TODO: 2018/7/25 关闭 113 | public void shutdown() { 114 | try { 115 | if (outputStream != null) outputStream.close(); 116 | if (inputStream != null) inputStream.close(); 117 | } catch (IOException e) { 118 | e.printStackTrace(); 119 | } 120 | clearSocket(); 121 | this.interrupt(); 122 | } 123 | 124 | private void clearSocket() { 125 | if (mSocket != null && (!mSocket.isClosed())) { 126 | try { 127 | mSocket.close(); 128 | mSocket = null; 129 | } catch (IOException e) { 130 | e.printStackTrace(); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/net/TcpUtil.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.net; 2 | 3 | import android.os.Build; 4 | 5 | /** 6 | * Created by wt 7 | * Date on 2018/7/4 09:48 8 | * tcp发送消息和文件,并接受回调的方法. 9 | */ 10 | public class TcpUtil { 11 | private static TcpUtil instance; 12 | private String ip; 13 | private int port; 14 | private int connectTime = 20000; 15 | private Type mType; 16 | 17 | private boolean isCancel = true; 18 | private RequestTcp requestTcp; 19 | 20 | private enum Type { 21 | DEFAULT, SETTING //向pc发送,向设置ip发送 22 | } 23 | 24 | private TcpUtil() { 25 | mType = Type.DEFAULT; 26 | } 27 | 28 | 29 | public TcpUtil(String ip, int port) { 30 | mType = Type.SETTING; 31 | this.ip = ip; 32 | this.port = port; 33 | } 34 | 35 | private static TcpUtil getInstance() { 36 | instance = new TcpUtil(); 37 | return instance; 38 | } 39 | 40 | public void sendMessage(int mainCmd, int subCmd, OnTcpSendMessageListner listner) { 41 | sendMessage(mainCmd, subCmd, null, listner); 42 | } 43 | 44 | public void sendMessage(int mainCmd, int subCmd, final String sendBody, OnTcpSendMessageListner listner) { 45 | sendMessage(mainCmd, subCmd, sendBody, new byte[0], connectTime, listner); 46 | } 47 | 48 | // TODO: 2018/7/16 wt 增加投屏者信息 49 | public void sendMessage(final int mainCmd, final int subCmd, final String sendBody, byte[] bytes, final int connectSoTime, final OnTcpSendMessageListner listner) { 50 | requestTcp = new RequestTcp(ip, port,mainCmd, subCmd, sendBody, bytes, connectSoTime, listner); 51 | requestTcp.start(); 52 | } 53 | 54 | public void sendTimeOut(int timeOut) { 55 | this.connectTime = timeOut; 56 | } 57 | 58 | 59 | public void cancel() { 60 | if (requestTcp != null) { 61 | requestTcp.shutdown(); 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/net/boastcast/NetWorkStateReceiver.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.net.boastcast; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.ConnectivityManager; 7 | import android.net.Network; 8 | import android.net.NetworkInfo; 9 | import android.os.Build; 10 | import android.support.annotation.RequiresApi; 11 | import android.util.Log; 12 | 13 | import com.test.screenimage.utils.NetWorkUtils; 14 | import com.test.screenimage.utils.ToastUtils; 15 | 16 | import org.greenrobot.eventbus.EventBus; 17 | 18 | /** 19 | * Created by wt on 2018/7/2. 20 | * 监听网络变化 21 | */ 22 | public class NetWorkStateReceiver extends BroadcastReceiver { 23 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 24 | @Override 25 | public void onReceive(Context context, Intent intent) { 26 | System.out.println("网络状态发生变化"); 27 | //检测API是不是小于23,因为到了API23之后getNetworkInfo(int networkType)方法被弃用 28 | if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { 29 | //获得ConnectivityManager对象 30 | ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 31 | //获取ConnectivityManager对象对应的NetworkInfo对象 32 | // 获取WIFI连接的信息 33 | NetworkInfo wifiNetworkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI); 34 | //获取移动数据连接的信息 35 | NetworkInfo ethNetInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_ETHERNET); 36 | if (wifiNetworkInfo == null && ethNetInfo == null) { 37 | EventBus.getDefault().post(""); 38 | ToastUtils.showShort(context, "请先连接网络!!"); 39 | } else if (wifiNetworkInfo != null && wifiNetworkInfo.isConnected()) { 40 | EventBus.getDefault().post(NetWorkUtils.getLocalIpAddress()); 41 | ToastUtils.showShort(context, "已连接无线网!!"); 42 | } else if (ethNetInfo != null && ethNetInfo.isConnected()) { 43 | EventBus.getDefault().post(NetWorkUtils.getLocalIpAddress()); 44 | ToastUtils.showShort(context, "已连接有线网!!"); 45 | } else { 46 | EventBus.getDefault().post(""); 47 | ToastUtils.showShort(context, "请先连接网络!!"); 48 | } 49 | return; 50 | } 51 | //API大于23时使用下面的方式进行网络监听 52 | //获得ConnectivityManager对象 53 | ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 54 | //获取所有网络连接的信息 55 | Network[] networks = connMgr.getAllNetworks(); 56 | //用于存放网络连接信息 57 | StringBuilder sb = new StringBuilder(); 58 | //通过循环将网络信息逐个取出来 59 | if (networks.length == 0) { 60 | EventBus.getDefault().post(""); 61 | ToastUtils.showShort(context, "请先连接网络!!"); 62 | return; 63 | } 64 | for (int i = 0; i < networks.length; i++) { 65 | //获取ConnectivityManager对象对应的NetworkInfo对象 66 | NetworkInfo networkInfo = connMgr.getNetworkInfo(networks[i]); 67 | if (networkInfo.isConnected()) { 68 | EventBus.getDefault().post(NetWorkUtils.getLocalIpAddress()); 69 | } else { 70 | EventBus.getDefault().post(""); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/screen/ScreenRecordEncoder.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.screen; 2 | 3 | import android.annotation.TargetApi; 4 | import android.media.MediaCodec; 5 | import android.os.Build; 6 | import android.os.Bundle; 7 | import android.os.Handler; 8 | import android.os.HandlerThread; 9 | import android.util.Log; 10 | import android.view.Surface; 11 | 12 | import com.test.screenimage.configuration.VideoConfiguration; 13 | import com.test.screenimage.constant.Constants; 14 | import com.test.screenimage.mediacodec.VideoMediaCodec; 15 | import com.test.screenimage.utils.SopCastLog; 16 | import com.test.screenimage.video.OnVideoEncodeListener; 17 | 18 | 19 | import java.nio.ByteBuffer; 20 | import java.util.concurrent.locks.ReentrantLock; 21 | 22 | import static android.os.Build.VERSION_CODES.LOLLIPOP; 23 | 24 | 25 | @TargetApi(LOLLIPOP) 26 | public class ScreenRecordEncoder { 27 | private MediaCodec mMediaCodec; 28 | private OnVideoEncodeListener mListener; 29 | private boolean mPause; 30 | private HandlerThread mHandlerThread; 31 | private Handler mEncoderHandler; 32 | private VideoConfiguration mConfiguration; 33 | private MediaCodec.BufferInfo mBufferInfo; 34 | private volatile boolean isStarted; 35 | private ReentrantLock encodeLock = new ReentrantLock(); 36 | 37 | public ScreenRecordEncoder(VideoConfiguration configuration) { 38 | mConfiguration = configuration; 39 | mMediaCodec = VideoMediaCodec.getVideoMediaCodec(mConfiguration); 40 | } 41 | 42 | public void setOnVideoEncodeListener(OnVideoEncodeListener listener) { 43 | mListener = listener; 44 | } 45 | 46 | public void setPause(boolean pause) { 47 | mPause = pause; 48 | } 49 | 50 | public void start() { 51 | mHandlerThread = new HandlerThread("LFEncode"); 52 | mHandlerThread.start(); 53 | mEncoderHandler = new Handler(mHandlerThread.getLooper()); 54 | mBufferInfo = new MediaCodec.BufferInfo(); 55 | mMediaCodec.start(); 56 | mEncoderHandler.post(swapDataRunnable); 57 | isStarted = true; 58 | } 59 | 60 | public Surface getSurface() { 61 | if(mMediaCodec != null) { 62 | return mMediaCodec.createInputSurface(); 63 | } else { 64 | return null; 65 | } 66 | } 67 | 68 | private Runnable swapDataRunnable = new Runnable() { 69 | @Override 70 | public void run() { 71 | drainEncoder(); 72 | } 73 | }; 74 | 75 | public void stop() { 76 | try { 77 | isStarted = false; 78 | mEncoderHandler.removeCallbacks(null); 79 | mHandlerThread.quit(); 80 | encodeLock.lock(); 81 | mMediaCodec.signalEndOfInputStream(); 82 | releaseEncoder(); 83 | encodeLock.unlock(); 84 | }catch (Exception e){ 85 | Log.e("ScreenRecordEncoder","92" + e.toString()); 86 | } 87 | } 88 | 89 | private void releaseEncoder() { 90 | if (mMediaCodec != null) { 91 | mMediaCodec.stop(); 92 | mMediaCodec.release(); 93 | mMediaCodec = null; 94 | } 95 | } 96 | 97 | @TargetApi(Build.VERSION_CODES.KITKAT) 98 | public void setRecorderBps(int bps) { 99 | if (mMediaCodec == null) { 100 | return; 101 | } 102 | SopCastLog.d(Constants.TAG, "bps :" + bps * 1024); 103 | // TODO: 2018/9/10 动态调整目标码率 104 | Bundle bitrate = new Bundle(); 105 | bitrate.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bps * 1024); 106 | mMediaCodec.setParameters(bitrate); 107 | } 108 | 109 | private void drainEncoder() { 110 | while (isStarted) { 111 | encodeLock.lock(); 112 | if(mMediaCodec != null) { 113 | int outBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 12000); 114 | if (outBufferIndex >= 0) { 115 | ByteBuffer bb = mMediaCodec.getOutputBuffer(outBufferIndex); 116 | if (mListener != null && !mPause) { 117 | mListener.onVideoEncode(bb, mBufferInfo); 118 | } 119 | mMediaCodec.releaseOutputBuffer(outBufferIndex, false); 120 | } else { 121 | try { 122 | // wait 10ms 123 | Thread.sleep(10); 124 | } catch (InterruptedException e) { 125 | e.printStackTrace(); 126 | } 127 | } 128 | encodeLock.unlock(); 129 | } else { 130 | encodeLock.unlock(); 131 | break; 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/service/ScreenImageService.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.service; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationChannel; 5 | import android.app.NotificationManager; 6 | import android.app.PendingIntent; 7 | import android.app.Service; 8 | import android.content.Intent; 9 | import android.content.ServiceConnection; 10 | import android.media.projection.MediaProjectionManager; 11 | import android.os.Binder; 12 | import android.os.Build; 13 | import android.os.IBinder; 14 | import android.support.annotation.Nullable; 15 | import android.support.v4.app.NotificationCompat; 16 | import android.util.Log; 17 | import android.widget.Toast; 18 | 19 | import com.test.screenimage.R; 20 | import com.test.screenimage.configuration.VideoConfiguration; 21 | import com.test.screenimage.constant.ScreenImageApi; 22 | import com.test.screenimage.controller.StreamController; 23 | import com.test.screenimage.controller.audio.NormalAudioController; 24 | import com.test.screenimage.controller.video.ScreenVideoController; 25 | import com.test.screenimage.stream.packer.TcpPacker; 26 | import com.test.screenimage.stream.sender.OnSenderListener; 27 | import com.test.screenimage.stream.sender.tcp.TcpSender; 28 | import com.test.screenimage.ui.ScreenImageActivity; 29 | import com.test.screenimage.utils.ToastUtils; 30 | 31 | /** 32 | * Created by wt 33 | * Date on 2018/7/20 16:14:02. 34 | * 35 | * @Desc 36 | */ 37 | 38 | public class ScreenImageService extends Service implements OnSenderListener { 39 | private static final String TAG = "ScreenRecorderService"; 40 | 41 | private TcpSender tcpSender; 42 | private VideoConfiguration mVideoConfiguration; 43 | private StreamController mStreamController; 44 | private OnSenderListener mListener; 45 | private int mCurrentBps; 46 | private int netBodCount = 0; 47 | 48 | private static final int foregroundId = 1234; 49 | NotificationManager notificationManager; 50 | Notification notification; 51 | String id = "screen_image_channel"; 52 | String name = "ScreenImage"; 53 | 54 | @Nullable 55 | @Override 56 | public IBinder onBind(Intent intent) { 57 | return new ScreenImageBinder(); 58 | } 59 | 60 | @Override 61 | public void onCreate() { 62 | super.onCreate(); 63 | notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 64 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 65 | NotificationChannel mChannel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW); 66 | notificationManager.createNotificationChannel(mChannel); 67 | } 68 | createNotification("...."); 69 | 70 | } 71 | 72 | private void createNotification(String text) { 73 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 74 | notification = new Notification.Builder(this) 75 | .setChannelId(id) 76 | .setContentTitle("ScreenImage 正在投屏中") 77 | .setContentText(text) 78 | .setContentIntent(getDefalutIntent(Notification.FLAG_FOREGROUND_SERVICE)) 79 | .setSmallIcon(R.mipmap.ic_launcher).build(); 80 | } else { 81 | NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this) 82 | .setContentTitle("ScreenImage 正在投屏中") 83 | .setContentText(text) 84 | .setSmallIcon(R.mipmap.ic_launcher) 85 | .setContentIntent(getDefalutIntent(Notification.FLAG_FOREGROUND_SERVICE)) 86 | .setOngoing(true); 87 | notification = notificationBuilder.build(); 88 | } 89 | notificationManager.notify(foregroundId, notification); 90 | } 91 | 92 | private PendingIntent getDefalutIntent(int flags) { 93 | Intent intent = new Intent(this, ScreenImageActivity.class); 94 | PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, intent, flags); 95 | return pendingIntent; 96 | } 97 | 98 | @Override 99 | public int onStartCommand(Intent intent, int flags, int startId) { 100 | startForeground(foregroundId, notification); 101 | return START_NOT_STICKY; 102 | 103 | } 104 | 105 | @Override 106 | public void onConnecting() { 107 | if (mListener != null) mListener.onConnecting(); 108 | } 109 | 110 | @Override 111 | public void onConnected() { 112 | mCurrentBps = mVideoConfiguration.maxBps; 113 | if (mListener != null) mListener.onConnected(); 114 | } 115 | 116 | @Override 117 | public void onDisConnected(String message) { 118 | if (mListener != null) mListener.onDisConnected(message); 119 | } 120 | 121 | @Override 122 | public void onConnectFail(String message) { 123 | if (mListener != null) mListener.onConnectFail(message); 124 | } 125 | 126 | @Override 127 | public void onPublishFail() { 128 | if (mListener != null) mListener.onPublishFail(); 129 | } 130 | 131 | @Override 132 | public void onNetGood() { 133 | //网络好 134 | netBodCount = 0; // 135 | // LogUtil.e(TAG, "onNetGood Current Bps: " + mCurrentBps); 136 | if (mCurrentBps == mVideoConfiguration.maxBps) { 137 | return; 138 | } 139 | int bps; 140 | if (mCurrentBps + 100 <= mVideoConfiguration.maxBps) { 141 | bps = mCurrentBps + 100; 142 | } else { 143 | bps = mVideoConfiguration.maxBps; 144 | } 145 | boolean result = mStreamController.setVideoBps(bps); 146 | if (result) { 147 | mCurrentBps = bps; 148 | } 149 | if (mListener != null) mListener.onNetGood(); 150 | } 151 | 152 | @Override 153 | public void onNetBad() { 154 | if (mCurrentBps == mVideoConfiguration.minBps) { 155 | netBodCount++; 156 | if (netBodCount >= 2) { 157 | netBodCount = 0; 158 | } 159 | return; 160 | } 161 | int bps; 162 | if (mCurrentBps - 550 >= mVideoConfiguration.minBps) { 163 | bps = mCurrentBps - 550; 164 | } else { 165 | bps = mVideoConfiguration.minBps; 166 | } 167 | boolean result = mStreamController.setVideoBps(bps); 168 | if (result) { 169 | mCurrentBps = bps; 170 | } 171 | if (mListener != null) mListener.onNetBad(); 172 | } 173 | 174 | @Override 175 | public void shutDown() { 176 | if (mListener != null) { 177 | mListener.shutDown(); 178 | } 179 | } 180 | 181 | @Override 182 | public void netSpeedChange(String netSpeedMsg) { 183 | createNotification(netSpeedMsg); 184 | if (mListener != null) mListener.netSpeedChange(netSpeedMsg); 185 | } 186 | 187 | public class ScreenImageBinder extends Binder { 188 | 189 | public ScreenImageService getService() { 190 | return ScreenImageService.this; 191 | } 192 | } 193 | 194 | /** 195 | * 开始录制屏幕 196 | * @param mMediaProjectionManage 197 | * @param resultCode 198 | * @param data 199 | * @param listener 200 | * @param ip 201 | * @param port 202 | */ 203 | public void startController(MediaProjectionManager mMediaProjectionManage, int resultCode, 204 | Intent data, OnSenderListener listener, String ip, int port) { 205 | ScreenVideoController screenVideoController = new ScreenVideoController(mMediaProjectionManage, resultCode, data); 206 | NormalAudioController audioController = new NormalAudioController(this); 207 | mStreamController = new StreamController(screenVideoController, audioController); 208 | TcpPacker packer = new TcpPacker(); 209 | mListener = listener; 210 | tcpSender = new TcpSender(ip, port); 211 | tcpSender.setSenderListener(this); 212 | tcpSender.setMianCmd(ScreenImageApi.RECORD.MAIN_CMD); 213 | tcpSender.setSubCmd(ScreenImageApi.RECORD.RECORDER_REQUEST_START); 214 | tcpSender.setSendBody(Build.MODEL); 215 | mVideoConfiguration = new VideoConfiguration.Builder().setSize(1080, 1920).build(); 216 | mStreamController.setVideoConfiguration(mVideoConfiguration); 217 | mStreamController.setPacker(packer); 218 | mStreamController.setSender(tcpSender); 219 | mStreamController.start(); 220 | tcpSender.openConnect(); 221 | } 222 | 223 | // @Override 224 | // public void unbindService(ServiceConnection conn) { 225 | // Log.e("ttt", "unbindService: zzz" ); 226 | // super.unbindService(conn); 227 | // if (mStreamController != null) { 228 | // mStreamController.stop(); 229 | // } 230 | // if (tcpSender != null) { 231 | // tcpSender.stop(); 232 | // } 233 | // } 234 | 235 | 236 | @Override 237 | public void onRebind(Intent intent) { 238 | super.onRebind(intent); 239 | } 240 | 241 | @Override 242 | public boolean onUnbind(Intent intent) { 243 | stopRecording(); 244 | return true; 245 | 246 | 247 | } 248 | 249 | private void stopRecording() { 250 | Log.e("ttt", "stopRecording: zzz"); 251 | if (mStreamController != null) { 252 | mStreamController.stop(); 253 | } 254 | shutDown(); 255 | stopForeground(true); 256 | } 257 | 258 | @Override 259 | public void onDestroy() { 260 | ToastUtils.show(this, "投屏服务停止了", Toast.LENGTH_SHORT); 261 | stopForeground(true);// 停止前台服务--参数:表示是否移除之前的通知 262 | super.onDestroy(); 263 | } 264 | 265 | } -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/packer/AnnexbHelper.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.packer; 2 | 3 | import android.media.MediaCodec; 4 | 5 | 6 | import com.test.screenimage.constant.Constants; 7 | import com.test.screenimage.utils.SopCastLog; 8 | 9 | import java.nio.ByteBuffer; 10 | import java.util.ArrayList; 11 | 12 | public class AnnexbHelper { 13 | 14 | // Coded slice of a non-IDR picture slice_layer_without_partitioning_rbsp( ) 15 | public final static int NonIDR = 1; 16 | // Coded slice of an IDR picture slice_layer_without_partitioning_rbsp( ) 17 | public final static int IDR = 5; 18 | // Supplemental enhancement information (SEI) sei_rbsp( ) 19 | public final static int SEI = 6; 20 | // Sequence parameter set seq_parameter_set_rbsp( ) 21 | public final static int SPS = 7; 22 | // Picture parameter set pic_parameter_set_rbsp( ) 23 | public final static int PPS = 8; 24 | // Access unit delimiter access_unit_delimiter_rbsp( ) 25 | public final static int AccessUnitDelimiter = 9; 26 | 27 | private AnnexbNaluListener mListener; 28 | 29 | private byte[] mPps; 30 | private byte[] mSps; 31 | private boolean mUploadPpsSps = true; 32 | 33 | 34 | /** 35 | * the search result for annexb. 36 | */ 37 | class AnnexbSearch { 38 | public int startCode = 0; 39 | public boolean match = false; 40 | } 41 | 42 | public interface AnnexbNaluListener { 43 | void onSpsPps(byte[] sps, byte[] pps); 44 | 45 | void onVideo(byte[] data, boolean isKeyFrame); 46 | } 47 | 48 | public void setAnnexbNaluListener(AnnexbNaluListener listener) { 49 | mListener = listener; 50 | } 51 | 52 | 53 | public void stop() { 54 | mListener = null; 55 | mPps = null; 56 | mSps = null; 57 | mUploadPpsSps = true; 58 | } 59 | 60 | // TODO: 2018/5/28 wt 处理h264数据,确保视频最开始是sps,pps 61 | private byte[] header = {0x00, 0x00, 0x00, 0x01}; 62 | public void analyseVideoDataonlyH264(ByteBuffer bb, MediaCodec.BufferInfo bi) { 63 | bb.position(bi.offset); 64 | bb.limit(bi.offset + bi.size); 65 | ArrayList frames = new ArrayList<>(); 66 | boolean isKeyFrame = false; 67 | 68 | while (bb.position() < bi.offset + bi.size) { 69 | byte[] frame = annexbDemux(bb, bi); 70 | if (frame == null) { 71 | SopCastLog.e(Constants.TAG, "annexb not match."); 72 | break; 73 | } 74 | // ignore the nalu type aud(9) 75 | if (isAccessUnitDelimiter(frame)) { 76 | continue; 77 | } 78 | // for pps 79 | if (isPps(frame)) { 80 | mPps = frame; 81 | continue; 82 | } 83 | // for sps 84 | if (isSps(frame)) { 85 | mSps = frame; 86 | continue; 87 | } 88 | // for IDR frame 89 | if (isKeyFrame(frame)) { 90 | isKeyFrame = true; 91 | } else { 92 | isKeyFrame = false; 93 | } 94 | //加上h.264的头 95 | frames.add(header); 96 | frames.add(frame); 97 | } 98 | if (mPps != null && mSps != null && mListener != null && mUploadPpsSps) { 99 | if (mListener != null) { 100 | mListener.onSpsPps(mSps, mPps); 101 | } 102 | mUploadPpsSps = false; 103 | } 104 | if (frames.size() == 0 || mListener == null) { 105 | return; 106 | } 107 | int size = 0; 108 | for (int i = 0; i < frames.size(); i++) { 109 | byte[] frame = frames.get(i); 110 | size += frame.length; 111 | } 112 | byte[] data = new byte[size]; 113 | int currentSize = 0; 114 | for (int i = 0; i < frames.size(); i++) { 115 | byte[] frame = frames.get(i); 116 | System.arraycopy(frame, 0, data, currentSize, frame.length); 117 | currentSize += frame.length; 118 | } 119 | if (mListener != null) { 120 | mListener.onVideo(data, isKeyFrame); 121 | } 122 | } 123 | 124 | /** 125 | * 从硬编出来的数据取出一帧nal 126 | * 127 | * @param bb 128 | * @param bi 129 | * @return 130 | */ 131 | private byte[] annexbDemux(ByteBuffer bb, MediaCodec.BufferInfo bi) { 132 | AnnexbSearch annexbSearch = new AnnexbSearch(); 133 | avcStartWithAnnexb(annexbSearch, bb, bi); 134 | 135 | if (!annexbSearch.match || annexbSearch.startCode < 3) { 136 | return null; 137 | } 138 | 139 | for (int i = 0; i < annexbSearch.startCode; i++) { 140 | bb.get(); 141 | } 142 | 143 | ByteBuffer frameBuffer = bb.slice(); 144 | int pos = bb.position(); 145 | while (bb.position() < bi.offset + bi.size) { 146 | avcStartWithAnnexb(annexbSearch, bb, bi); 147 | if (annexbSearch.match) { 148 | break; 149 | } 150 | bb.get(); 151 | } 152 | 153 | int size = bb.position() - pos; 154 | byte[] frameBytes = new byte[size]; 155 | frameBuffer.get(frameBytes); 156 | return frameBytes; 157 | } 158 | 159 | 160 | /** 161 | * 从硬编出来的byteBuffer中查找nal 162 | * 163 | * @param as 164 | * @param bb 165 | * @param bi 166 | */ 167 | private void avcStartWithAnnexb(AnnexbSearch as, ByteBuffer bb, MediaCodec.BufferInfo bi) { 168 | as.match = false; 169 | as.startCode = 0; 170 | int pos = bb.position(); 171 | while (pos < bi.offset + bi.size - 3) { 172 | // not match. 173 | if (bb.get(pos) != 0x00 || bb.get(pos + 1) != 0x00) { 174 | break; 175 | } 176 | 177 | // match N[00] 00 00 01, where N>=0 178 | if (bb.get(pos + 2) == 0x01) { 179 | as.match = true; 180 | as.startCode = pos + 3 - bb.position(); 181 | break; 182 | } 183 | pos++; 184 | } 185 | } 186 | 187 | private byte[] buildNaluHeader(int length) { 188 | ByteBuffer buffer = ByteBuffer.allocate(4); 189 | buffer.putInt(length); 190 | return buffer.array(); 191 | } 192 | 193 | 194 | private boolean isSps(byte[] frame) { 195 | if (frame.length < 1) { 196 | return false; 197 | } 198 | // 5bits, 7.3.1 NAL unit syntax, 199 | // H.264-AVC-ISO_IEC_14496-10.pdf, page 44. 200 | // 7: SPS, 8: PPS, 5: I Frame, 1: P Frame 201 | int nal_unit_type = (frame[0] & 0x1f); 202 | return nal_unit_type == SPS; 203 | } 204 | 205 | private boolean isPps(byte[] frame) { 206 | if (frame.length < 1) { 207 | return false; 208 | } 209 | // 5bits, 7.3.1 NAL unit syntax, 210 | // H.264-AVC-ISO_IEC_14496-10.pdf, page 44. 211 | // 7: SPS, 8: PPS, 5: I Frame, 1: P Frame 212 | int nal_unit_type = (frame[0] & 0x1f); 213 | return nal_unit_type == PPS; 214 | } 215 | 216 | private boolean isKeyFrame(byte[] frame) { 217 | if (frame.length < 1) { 218 | return false; 219 | } 220 | // 5bits, 7.3.1 NAL unit syntax, 221 | // H.264-AVC-ISO_IEC_14496-10.pdf, page 44. 222 | // 7: SPS, 8: PPS, 5: I Frame, 1: P Frame 223 | int nal_unit_type = (frame[0] & 0x1f); 224 | return nal_unit_type == IDR; 225 | } 226 | 227 | private static boolean isAccessUnitDelimiter(byte[] frame) { 228 | if (frame.length < 1) { 229 | return false; 230 | } 231 | // 5bits, 7.3.1 NAL unit syntax, 232 | // H.264-AVC-ISO_IEC_14496-10.pdf, page 44. 233 | // 7: SPS, 8: PPS, 5: I Frame, 1: P Frame 234 | int nal_unit_type = (frame[0] & 0x1f); 235 | return nal_unit_type == AccessUnitDelimiter; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/packer/Packer.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.packer; 2 | 3 | import android.media.MediaCodec; 4 | 5 | import java.nio.ByteBuffer; 6 | 7 | public interface Packer { 8 | interface OnPacketListener { 9 | //第一个参数为打包后的数据,第二个为自定义打包后的类型 10 | void onPacket(byte[] data, int packetType); 11 | 12 | //回调sps和pps 13 | void onSpsPps(byte[] mSpsPps); 14 | } 15 | 16 | //设置打包监听器 17 | void setPacketListener(OnPacketListener listener); 18 | 19 | //处理视频硬编编码器输出的数据 20 | void onVideoData(ByteBuffer bb, MediaCodec.BufferInfo bi); 21 | 22 | //处理音频硬编编码器输出的数据 23 | void onAudioData(ByteBuffer bb, MediaCodec.BufferInfo bi); 24 | 25 | //开始打包,一般进行打包的预处理 26 | void start(); 27 | 28 | //结束打包,一般进行打包器的状态恢复 29 | void stop(); 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/packer/TcpPacker.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.packer; 2 | 3 | import android.media.MediaCodec; 4 | import android.util.Log; 5 | 6 | 7 | import com.test.screenimage.configuration.AudioConfiguration; 8 | 9 | import java.nio.ByteBuffer; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | /** 14 | * Created by wt 15 | * Date on 2018/5/28 16 | * 17 | * @Desc add 18 | */ 19 | 20 | public class TcpPacker implements Packer, AnnexbHelper.AnnexbNaluListener { 21 | // private static final String TAG = "TcpPacker"; 22 | // public static final int HEADER = 0; 23 | // public static final int METADATA = 1; 24 | public static final int FIRST_VIDEO = 2; 25 | public static final int AUDIO = 4; 26 | public static final int KEY_FRAME = 5; 27 | public static final int INTER_FRAME = 6; 28 | 29 | private OnPacketListener packetListener; 30 | private boolean isHeaderWrite; 31 | private boolean isKeyFrameWrite; 32 | 33 | private int mAudioSampleRate, mAudioSampleSize; 34 | private boolean mIsStereo; 35 | private boolean mSendAudio = false; 36 | 37 | private AnnexbHelper mAnnexbHelper; 38 | // TODO: 2018/5/28 wt 增添h,264数据头 39 | private byte[] mSpsPps; 40 | private byte[] header = {0x00, 0x00, 0x00, 0x01}; //H264的头文件 41 | 42 | 43 | public TcpPacker() { 44 | mAnnexbHelper = new AnnexbHelper(); 45 | } 46 | 47 | public void setPacketListener(OnPacketListener listener) { 48 | packetListener = listener; 49 | } 50 | 51 | 52 | @Override 53 | public void start() { 54 | mAnnexbHelper.setAnnexbNaluListener(this); 55 | } 56 | 57 | @Override 58 | public void onVideoData(ByteBuffer bb, MediaCodec.BufferInfo bi) { 59 | mAnnexbHelper.analyseVideoDataonlyH264(bb, bi); 60 | } 61 | 62 | @Override 63 | public void onAudioData(ByteBuffer bb, MediaCodec.BufferInfo bi) { 64 | if (packetListener == null) { 65 | return; 66 | } 67 | if (!mSendAudio) { 68 | return; 69 | } 70 | bb.position(bi.offset); 71 | bb.limit(bi.offset + bi.size); 72 | byte[] audio = new byte[bi.size]; 73 | bb.get(audio); 74 | //一般第一帧都是2个字节 75 | int length = 7 + audio.length; 76 | ByteBuffer tempBb = ByteBuffer.allocate(length + 4); 77 | tempBb.put(header); 78 | tempBb.put(getADTSHeader(length)); 79 | tempBb.put(audio); 80 | packetListener.onPacket(tempBb.array(), AUDIO); 81 | } 82 | 83 | @Override 84 | public void stop() { 85 | isHeaderWrite = false; 86 | isKeyFrameWrite = false; 87 | mAnnexbHelper.stop(); 88 | } 89 | 90 | 91 | // TODO: 2018/6/4 wt h.264裸流 92 | @Override 93 | public void onSpsPps(byte[] sps, byte[] pps) { 94 | ByteBuffer byteBuffer = ByteBuffer.allocate(sps.length + 4); 95 | byteBuffer.put(header); 96 | byteBuffer.put(sps); 97 | mSpsPps = byteBuffer.array(); 98 | 99 | packetListener.onPacket(mSpsPps, FIRST_VIDEO); 100 | ByteBuffer byteBuffer1 = ByteBuffer.allocate(pps.length + 4); 101 | byteBuffer1.put(header); 102 | byteBuffer1.put(pps); 103 | packetListener.onPacket(byteBuffer1.array(), FIRST_VIDEO); 104 | isHeaderWrite = true; 105 | } 106 | 107 | 108 | @Override 109 | public void onVideo(byte[] video, boolean isKeyFrame) { 110 | if (packetListener == null || !isHeaderWrite) { 111 | return; 112 | } 113 | int packetType = INTER_FRAME; 114 | if (isKeyFrame) { 115 | isKeyFrameWrite = true; 116 | packetType = KEY_FRAME; 117 | } 118 | //确保第一帧是关键帧,避免一开始出现灰色模糊界面 119 | if (!isKeyFrameWrite) { 120 | return; 121 | } 122 | ByteBuffer bb; 123 | if (isKeyFrame) { 124 | bb = ByteBuffer.allocate(video.length); 125 | bb.put(video); 126 | } else { 127 | bb = ByteBuffer.allocate(video.length); 128 | bb.put(video); 129 | } 130 | packetListener.onPacket(bb.array(), packetType); 131 | } 132 | 133 | public void initAudioParams(int sampleRate, int sampleSize, boolean isStereo) { 134 | mAudioSampleRate = sampleRate; 135 | mAudioSampleSize = sampleSize; 136 | mIsStereo = isStereo; 137 | } 138 | 139 | // TODO: 2018/6/4 是否发送音频 140 | public void setSendAudio(boolean sendAudio) { 141 | this.mSendAudio = sendAudio; 142 | } 143 | 144 | // TODO: 2018/6/4 wt 给编码出的aac裸流添加adts头字段 145 | private byte[] getADTSHeader(int packetLen) { 146 | byte[] packet = new byte[7]; 147 | int profile = 2; //AAC LC 148 | int freqIdx = getFrequencyIdx(AudioConfiguration.DEFAULT_FREQUENCY); //44100Hz 149 | int chanCfg = 2; //CPE 声道数 150 | packet[0] = (byte) 0xFF; 151 | packet[1] = (byte) 0xF9; 152 | packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2)); 153 | packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11)); 154 | packet[4] = (byte) ((packetLen & 0x7FF) >> 3); 155 | packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F); 156 | packet[6] = (byte) 0xFC; 157 | return packet; 158 | } 159 | 160 | // TODO: 2018/6/5 wt 找出采集频率对应下标 161 | private int getFrequencyIdx(int defaultFrequency) { 162 | Map samplingFrequencyIndexMap = new HashMap<>(); 163 | samplingFrequencyIndexMap.put(96000, 0); 164 | samplingFrequencyIndexMap.put(88200, 1); 165 | samplingFrequencyIndexMap.put(64000, 2); 166 | samplingFrequencyIndexMap.put(48000, 3); 167 | samplingFrequencyIndexMap.put(44100, 4); 168 | samplingFrequencyIndexMap.put(32000, 5); 169 | samplingFrequencyIndexMap.put(24000, 6); 170 | samplingFrequencyIndexMap.put(22050, 7); 171 | samplingFrequencyIndexMap.put(16000, 8); 172 | samplingFrequencyIndexMap.put(12000, 9); 173 | samplingFrequencyIndexMap.put(11025, 10); 174 | samplingFrequencyIndexMap.put(8000, 11); 175 | return samplingFrequencyIndexMap.get(defaultFrequency); 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/OnSenderListener.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender; 2 | 3 | /** 4 | * Created by wt 5 | * Date on 2018/5/28 6 | * 7 | * @Desc 发送监听 8 | */ 9 | 10 | public interface OnSenderListener { 11 | void onConnecting(); 12 | 13 | void onConnected(); 14 | 15 | void onDisConnected(String message); 16 | 17 | void onConnectFail(String message); 18 | 19 | void onPublishFail(); 20 | 21 | void onNetGood(); 22 | 23 | void onNetBad(); 24 | 25 | // TODO: 2018/7/25 手动断开连接 26 | void shutDown(); 27 | 28 | void netSpeedChange(String netSpeedMsg); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/Sender.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender; 2 | 3 | public interface Sender { 4 | void start(); 5 | void onData(byte[] data, int type); 6 | void stop(); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/sendqueue/ISendQueue.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.sendqueue; 2 | 3 | 4 | import com.test.screenimage.entity.Frame; 5 | 6 | public interface ISendQueue { 7 | void start(); 8 | void stop(); 9 | void setBufferSize(int size); 10 | void putFrame(Frame frame); 11 | Frame takeFrame(); 12 | void setSendQueueListener(SendQueueListener listener); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/sendqueue/NormalSendQueue.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.sendqueue; 2 | 3 | import android.util.Log; 4 | 5 | import com.test.screenimage.constant.Constants; 6 | import com.test.screenimage.entity.Frame; 7 | import com.test.screenimage.utils.SopCastLog; 8 | 9 | 10 | import java.util.ArrayList; 11 | import java.util.concurrent.ArrayBlockingQueue; 12 | import java.util.concurrent.atomic.AtomicInteger; 13 | 14 | import static com.test.screenimage.entity.Frame.FRAME_TYPE_AUDIO; 15 | import static com.test.screenimage.entity.Frame.FRAME_TYPE_INTER_FRAME; 16 | import static com.test.screenimage.entity.Frame.FRAME_TYPE_KEY_FRAME; 17 | 18 | public class NormalSendQueue implements ISendQueue { 19 | // private static final int NORMAL_FRAME_BUFFER_SIZE = 800; 20 | // TODO modify by wt tcp发送缓存区大小从800改为150 21 | //-------------------------------------------------------------- 22 | private static final int NORMAL_FRAME_BUFFER_SIZE = 150; //缓存区大小 23 | private static final int SCAN_MAX_TIME = 5; //仲裁次数,每循环SCAN_MAX_TIME 次,每次sleep(DEFAULT_SLEEP_TIME),会执行一次检查网速的代码 24 | private static final int DEFAULT_SLEEP_TIME = 200; // 25 | private static final int DEFAULT_NEGATIVE_COUNT = 3; //循环SCAN_MAX_TIME 次,有 DEFAULT_NEGATIVE_COUNT 次输入queue的帧小于取走的帧 26 | //----------------------------------------------------------- 27 | private ArrayBlockingQueue mFrameBuffer; 28 | private int mFullQueueCount = NORMAL_FRAME_BUFFER_SIZE; 29 | private AtomicInteger mTotalFrameCount = new AtomicInteger(0); //总个数 30 | private AtomicInteger mGiveUpFrameCount = new AtomicInteger(0); //总个数 31 | private AtomicInteger mKeyFrameCount = new AtomicInteger(0); //队列里Key帧的总个数... 32 | 33 | private AtomicInteger mInFrameCount = new AtomicInteger(0); //进入总个数 34 | private AtomicInteger mOutFrameCount = new AtomicInteger(0); //输出总个数 35 | private volatile boolean mScanFlag; 36 | private SendQueueListener mSendQueueListener; 37 | private ScanThread mScanThread; 38 | private boolean isDebug = false; 39 | ; 40 | 41 | public NormalSendQueue() { 42 | mFrameBuffer = new ArrayBlockingQueue<>(mFullQueueCount, true); 43 | } 44 | 45 | @Override 46 | public void start() { 47 | mScanFlag = true; 48 | mScanThread = new ScanThread(); 49 | mScanThread.start(); 50 | } 51 | 52 | @Override 53 | public void stop() { 54 | mScanFlag = false; 55 | mInFrameCount.set(0); 56 | mOutFrameCount.set(0); 57 | mTotalFrameCount.set(0); 58 | mGiveUpFrameCount.set(0); 59 | mFrameBuffer.clear(); 60 | } 61 | 62 | public void setSendQueueListener(SendQueueListener listener) { 63 | mSendQueueListener = listener; 64 | } 65 | 66 | @Override 67 | public void setBufferSize(int size) { 68 | mFullQueueCount = size; 69 | } 70 | 71 | public int getBufferFrameCount() { 72 | return mTotalFrameCount.get(); 73 | } 74 | 75 | public int getGiveUpFrameCount() { 76 | return mGiveUpFrameCount.get(); 77 | } 78 | 79 | @Override 80 | public void putFrame(Frame frame) { 81 | if (mFrameBuffer == null) { 82 | return; 83 | } 84 | if (frame.frameType == FRAME_TYPE_KEY_FRAME) mKeyFrameCount.getAndIncrement(); 85 | abandonData(); 86 | try { 87 | mFrameBuffer.put(frame); 88 | showLog("put frame total = " + mFrameBuffer.size()); 89 | mInFrameCount.getAndIncrement(); 90 | mTotalFrameCount.getAndIncrement(); 91 | } catch (InterruptedException e) { 92 | showLog("put Frame exception" + e.toString()); 93 | e.printStackTrace(); 94 | } 95 | } 96 | 97 | @Override 98 | public Frame takeFrame() { 99 | if (mFrameBuffer == null) { 100 | return null; 101 | } 102 | Frame frame = null; 103 | try { 104 | showLog("take frame total size = " + mFrameBuffer.size()); 105 | frame = mFrameBuffer.take(); 106 | if (frame.frameType == FRAME_TYPE_KEY_FRAME) mKeyFrameCount.getAndDecrement(); 107 | mOutFrameCount.getAndIncrement(); 108 | mTotalFrameCount.getAndDecrement(); 109 | } catch (InterruptedException e) { 110 | //do nothing 111 | showLog("take Frame exception" + e.toString()); 112 | } 113 | return frame; 114 | } 115 | 116 | private void abandonData() { 117 | if (mTotalFrameCount.get() >= (mFullQueueCount / 3)) { 118 | showLog("队列里的帧数太多,开始丢帧.."); 119 | //从队列头部开始搜索,删除最早发现的连续P帧 120 | boolean pFrameDelete = false; 121 | boolean start = false; 122 | for (Frame frame : mFrameBuffer) { 123 | if (!start) showLog("丢掉了下一个KEY_FRAME前的所有INTER_FRAME.."); 124 | if (frame.frameType == FRAME_TYPE_INTER_FRAME) { 125 | start = true; 126 | } 127 | if (start) { 128 | if (frame.frameType == FRAME_TYPE_INTER_FRAME) { 129 | mFrameBuffer.remove(frame); 130 | mTotalFrameCount.getAndDecrement(); 131 | mGiveUpFrameCount.getAndIncrement(); 132 | pFrameDelete = true; 133 | } else if (frame.frameType == 3) { 134 | if (mKeyFrameCount.get() > 5) { 135 | showLog("丢掉了一个关键帧.. total" + mKeyFrameCount.get()); 136 | mFrameBuffer.remove(frame); 137 | mKeyFrameCount.getAndDecrement(); 138 | continue; 139 | } 140 | break; 141 | } 142 | } 143 | } 144 | boolean kFrameDelete = false; 145 | //从队列头部开始搜索,删除最早发现的I帧 146 | if (!pFrameDelete) { 147 | for (Frame frame : mFrameBuffer) { 148 | if (frame.frameType == FRAME_TYPE_KEY_FRAME) { 149 | mFrameBuffer.remove(frame); 150 | showLog("丢掉了一个关键帧.."); 151 | mTotalFrameCount.getAndDecrement(); 152 | mGiveUpFrameCount.getAndIncrement(); 153 | mKeyFrameCount.getAndDecrement(); 154 | kFrameDelete = true; 155 | break; 156 | } 157 | } 158 | } 159 | //从队列头部开始搜索,删除音频 160 | if (!pFrameDelete && !kFrameDelete) { 161 | for (Frame frame : mFrameBuffer) { 162 | if (frame.frameType == FRAME_TYPE_AUDIO) { 163 | mFrameBuffer.remove(frame); 164 | mTotalFrameCount.getAndDecrement(); 165 | mGiveUpFrameCount.getAndIncrement(); 166 | break; 167 | } 168 | } 169 | } 170 | } 171 | } 172 | 173 | private class ScanThread extends Thread { 174 | 175 | private int mCurrentScanTime = 0; 176 | private ArrayList mScanSnapShotList = new ArrayList<>(); 177 | 178 | @Override 179 | public void run() { 180 | while (mScanFlag) { 181 | //达到仲裁次数了 182 | if (mCurrentScanTime == SCAN_MAX_TIME) { 183 | int averageDif = 0; 184 | int negativeCounter = 0; 185 | String strLog = ""; 186 | for (int i = 0; i < SCAN_MAX_TIME; i++) { 187 | int dif = mScanSnapShotList.get(i).outCount - mScanSnapShotList.get(i).inCount; 188 | if (dif < 0) { 189 | negativeCounter++; 190 | } 191 | averageDif += dif; 192 | strLog = strLog + String.format("n%d:%d ", i, dif); 193 | } 194 | SopCastLog.e(Constants.TAG, strLog); 195 | if (negativeCounter >= DEFAULT_NEGATIVE_COUNT || averageDif < -100) { 196 | //坏 197 | SopCastLog.d(Constants.TAG, "Bad Send Speed."); 198 | if (mSendQueueListener != null) { 199 | mSendQueueListener.bad(); 200 | } 201 | } else { 202 | //好 203 | SopCastLog.e(Constants.TAG, "Good Send Speed."); 204 | if (mSendQueueListener != null) { 205 | mSendQueueListener.good(); 206 | } 207 | } 208 | //清空 209 | mScanSnapShotList.clear(); 210 | 211 | } 212 | mScanSnapShotList.add(new ScanSnapShot(mInFrameCount.get(), mOutFrameCount.get())); 213 | mInFrameCount.set(0); 214 | mOutFrameCount.set(0); 215 | mCurrentScanTime++; 216 | try { 217 | Thread.sleep(DEFAULT_SLEEP_TIME); 218 | } catch (InterruptedException e) { 219 | e.printStackTrace(); 220 | } 221 | } 222 | } 223 | } 224 | 225 | private class ScanSnapShot { 226 | public int inCount; 227 | public int outCount; 228 | 229 | public ScanSnapShot(int inCount, int outCount) { 230 | this.inCount = inCount; 231 | this.outCount = outCount; 232 | } 233 | } 234 | 235 | private void showLog(String msg) { 236 | if (isDebug) Log.e("NormalSendQueue", "" + msg); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/sendqueue/SendQueueListener.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.sendqueue; 2 | public interface SendQueueListener { 3 | void good(); 4 | void bad(); 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/tcp/EncodeV1.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.tcp; 2 | 3 | import android.os.Build; 4 | import android.text.TextUtils; 5 | import android.util.Log; 6 | 7 | import com.test.screenimage.constant.ScreenImageApi; 8 | import com.test.screenimage.utils.ByteUtil; 9 | 10 | import java.nio.ByteBuffer; 11 | 12 | /** 13 | * Created by wt 14 | * Date on 2018/6/4 . 15 | * 传输数据格式 16 | */ 17 | 18 | public class EncodeV1 { 19 | private int mainCmd; 20 | private int subCmd; 21 | private String sendBody;//要发送的内容 22 | private byte[] sendBuffer; //要发送的音视频内容 23 | 24 | 25 | /** 26 | * by wt 27 | * 28 | * @param mainCmd 主指令 29 | * @param subCmd 子指令 30 | * @param sendBody 文本内容 31 | * @param sendBuffer 音视频内容 32 | */ 33 | public EncodeV1(int mainCmd, int subCmd, String sendBody, byte[] sendBuffer) { 34 | this.mainCmd = mainCmd; 35 | this.subCmd = subCmd; 36 | this.sendBody = sendBody; 37 | this.sendBuffer = sendBuffer; 38 | } 39 | 40 | public byte[] buildSendContent() { 41 | int bodyLength = 0; 42 | int bodyByte = 0; 43 | ByteBuffer bb = null; 44 | //文本数据 45 | if (!TextUtils.isEmpty(sendBody)) { 46 | bodyLength = sendBody.getBytes().length; 47 | } 48 | //音视频数据 49 | if (sendBuffer.length != 0) { 50 | bodyByte = sendBuffer.length; 51 | } 52 | //创建内存缓冲区 53 | bb = ByteBuffer.allocate(18 + bodyLength + bodyByte); 54 | bb.put(ScreenImageApi.encodeVersion1); //0-1编码版本 55 | bb.put(ByteUtil.int2Bytes(mainCmd)); //1-5 主指令 56 | bb.put(ByteUtil.int2Bytes(subCmd)); //5-9 子指令 57 | bb.put(ByteUtil.int2Bytes(bodyLength)); //9-13位,文本数据长度 58 | bb.put(ByteUtil.int2Bytes(bodyByte)); //13-17位,音视频数据长度 59 | byte[] tempb = bb.array(); 60 | bb.put(ByteUtil.getCheckCode(tempb)); 61 | //数据字节数组 62 | if (bodyLength != 0) { 63 | bb.put(sendBody.getBytes()); 64 | } 65 | if (sendBuffer.length != 0) { 66 | bb.put(sendBuffer); 67 | } 68 | return bb.array(); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/tcp/TcpConnection.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.tcp; 2 | 3 | import android.util.Log; 4 | 5 | import com.test.screenimage.configuration.VideoConfiguration; 6 | import com.test.screenimage.entity.ReceiveData; 7 | import com.test.screenimage.stream.sender.sendqueue.ISendQueue; 8 | import com.test.screenimage.stream.sender.tcp.interf.OnTcpReadListener; 9 | import com.test.screenimage.stream.sender.tcp.interf.OnTcpWriteListener; 10 | import com.test.screenimage.stream.sender.tcp.interf.TcpConnectListener; 11 | 12 | 13 | import java.io.BufferedInputStream; 14 | import java.io.BufferedOutputStream; 15 | import java.io.IOException; 16 | import java.net.InetSocketAddress; 17 | import java.net.Socket; 18 | import java.net.SocketAddress; 19 | 20 | /** 21 | * Created by wt 22 | * Date on 2018/5/28 23 | * 24 | * @Desc socket/tcp 连接 25 | */ 26 | 27 | public class TcpConnection implements OnTcpReadListener, OnTcpWriteListener { 28 | private TcpConnectListener listener; 29 | private static final String TAG = "TcpConnection"; 30 | private Socket socket; 31 | private ISendQueue mSendQueue; 32 | private TcpWriteThread mWrite; 33 | private TcpReadThread mRead; 34 | private BufferedInputStream in; 35 | private BufferedOutputStream out; 36 | private int width, height; 37 | private int maxBps; 38 | private int fps; 39 | private byte[] mSpsPps; 40 | 41 | 42 | public void setConnectListener(TcpConnectListener listener) { 43 | this.listener = listener; 44 | } 45 | 46 | public void setSendQueue(ISendQueue sendQueue) { 47 | mSendQueue = sendQueue; 48 | } 49 | 50 | //连接服务端 51 | public void connect(String ip, int port, int mainCmd, int subCmd, String sendBody) { 52 | socket = new Socket(); 53 | SocketAddress socketAddress = new InetSocketAddress(ip, port); 54 | try { 55 | socket.connect(socketAddress, 20000); 56 | //tcp连接成功后is.read阻塞多长时间 57 | socket.setSoTimeout(60000); 58 | } catch (IOException e) { 59 | e.printStackTrace(); 60 | if (listener != null) { 61 | listener.onSocketConnectFail(e.getMessage()); 62 | return; 63 | } 64 | } 65 | listener.onSocketConnectSuccess(); 66 | if (listener == null || socket == null || !socket.isConnected()) { 67 | listener.onSocketConnectFail(null); 68 | 69 | return; 70 | } 71 | try { 72 | // 获取当前连接的输出流 73 | out = new BufferedOutputStream(socket.getOutputStream()); 74 | // 获取当前连接的输入流 75 | in = new BufferedInputStream(socket.getInputStream()); 76 | mWrite = new TcpWriteThread(out, mSendQueue, mainCmd, subCmd, sendBody); 77 | mWrite.setOnTcpWriteThread(this); 78 | mRead = new TcpReadThread(in); 79 | mRead.setOnTcpReadListener(this); 80 | mRead.start(); 81 | listener.onTcpConnectSuccess(); 82 | mWrite.sendStartBuff(); 83 | } catch (IOException e) { 84 | e.printStackTrace(); 85 | listener.onTcpConnectFail(e.getMessage()); 86 | } 87 | } 88 | 89 | public void setVideoParams(VideoConfiguration videoConfiguration) { 90 | width = videoConfiguration.width; 91 | height = videoConfiguration.height; 92 | fps = videoConfiguration.fps; 93 | maxBps = videoConfiguration.maxBps; 94 | } 95 | 96 | public void setSpsPps(byte[] spsPps) { 97 | this.mSpsPps = spsPps; 98 | } 99 | 100 | @Override 101 | public void socketDisconnect(String message) { 102 | //与服务端连接断开 103 | listener.onSocketDisconnect(message); 104 | } 105 | 106 | @Override 107 | public void netSpeedChange(String msg) { 108 | if (listener != null) listener.onNetSpeedChange(msg); 109 | } 110 | 111 | @Override 112 | public void connectSuccess(ReceiveData data) { 113 | //收到数据后,解析后得到数据 114 | Log.e(TAG, "connectSuccess: " + data.getHeader().getSubCmd()); 115 | if (data == null) { 116 | return; 117 | } 118 | int subCmd = data.getHeader().getSubCmd(); 119 | switch (subCmd) { 120 | case 0x01: 121 | //连接成功,开启发送线程 122 | mWrite.start(); 123 | break; 124 | } 125 | } 126 | 127 | 128 | public void stop() { 129 | new Thread() { 130 | @Override 131 | public void run() { 132 | super.run(); 133 | try { 134 | Log.i("TcpConnecttion", "stop"); 135 | if (out != null) out.close(); 136 | if (in != null) in.close(); 137 | } catch (IOException e) { 138 | e.printStackTrace(); 139 | } 140 | clearSocket(); 141 | if (mWrite != null) { 142 | mWrite.setOnTcpWriteThread(null); 143 | mWrite.shutDown(); 144 | } 145 | if (mRead != null) { 146 | mRead.setOnTcpReadListener(null); 147 | mRead.shutDown(); 148 | } 149 | } 150 | }.start(); 151 | } 152 | 153 | private void clearSocket() { 154 | if (socket != null && socket.isConnected()) { 155 | try { 156 | socket.close(); 157 | Log.i(TAG, "socket close"); 158 | socket = null; 159 | } catch (Exception e) { 160 | e.printStackTrace(); 161 | } 162 | } 163 | } 164 | 165 | 166 | } 167 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/tcp/TcpReadThread.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.tcp; 2 | 3 | import android.os.SystemClock; 4 | import android.text.TextUtils; 5 | import android.util.Log; 6 | 7 | import com.test.screenimage.constant.ScreenImageApi; 8 | import com.test.screenimage.entity.ReceiveData; 9 | import com.test.screenimage.entity.ReceiveHeader; 10 | import com.test.screenimage.stream.sender.tcp.interf.OnTcpReadListener; 11 | import com.test.screenimage.utils.AnalyticDataUtils; 12 | import com.test.screenimage.utils.ByteUtil; 13 | import com.test.screenimage.utils.SopCastLog; 14 | 15 | 16 | import java.io.BufferedInputStream; 17 | import java.io.ByteArrayOutputStream; 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | 21 | /** 22 | * Created by wt 23 | * Date on 2018/5/28 24 | * 25 | * @Desc 处理流消息 26 | */ 27 | 28 | public class TcpReadThread extends Thread implements AnalyticDataUtils.OnAnalyticDataListener { 29 | private final static String TAG = "wt"; 30 | private BufferedInputStream bis; 31 | private AnalyticDataUtils mAnalyticDataUtils; 32 | private OnTcpReadListener mListener; 33 | private volatile boolean startFlag; 34 | 35 | public TcpReadThread(BufferedInputStream bis) { 36 | this.bis = bis; 37 | mAnalyticDataUtils = new AnalyticDataUtils(); 38 | mAnalyticDataUtils.setOnAnalyticDataListener(this); 39 | startFlag = true; 40 | } 41 | 42 | public void setOnTcpReadListener(OnTcpReadListener listener) { 43 | this.mListener = listener; 44 | } 45 | 46 | @Override 47 | public void run() { 48 | super.run(); 49 | while (startFlag) { 50 | SystemClock.sleep(50); 51 | try { 52 | acceptMsg(); 53 | } catch (IOException e) { 54 | startFlag = false; 55 | if (mListener != null) mListener.socketDisconnect(e.getMessage()); 56 | } 57 | } 58 | } 59 | 60 | 61 | // TODO: 2018/6/6 wt 接收到服务端发来的消息指令 62 | public void acceptMsg() throws IOException { 63 | if (mListener == null) { 64 | return; 65 | } 66 | if (bis.available() <= 0) { 67 | return; 68 | } 69 | byte[] header = mAnalyticDataUtils.readByte(bis, 18); 70 | //根据协议分析数据头 71 | ReceiveHeader receiveHeader = mAnalyticDataUtils.analysisHeader(header); 72 | if (receiveHeader.getStringBodylength() == 0 && receiveHeader.getBuffSize() == 0) { 73 | SopCastLog.e("wtt", "接收数据为空"); 74 | return; 75 | } 76 | if (receiveHeader.getEncodeVersion() != ScreenImageApi.encodeVersion1) { 77 | SopCastLog.e("wtt", "收到的消息无法解析"); 78 | return; 79 | } 80 | //解析数据 81 | mAnalyticDataUtils.analyticData(bis, receiveHeader); 82 | } 83 | 84 | /** 85 | * 停止读 86 | */ 87 | public void shutDown() { 88 | startFlag = false; 89 | this.interrupt(); 90 | } 91 | 92 | @Override 93 | public void onSuccess(ReceiveData data) { 94 | if (mListener != null) mListener.connectSuccess(data); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/tcp/TcpSender.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.tcp; 2 | 3 | import android.text.TextUtils; 4 | import android.util.Log; 5 | 6 | import com.test.screenimage.configuration.VideoConfiguration; 7 | import com.test.screenimage.entity.Frame; 8 | import com.test.screenimage.stream.packer.TcpPacker; 9 | import com.test.screenimage.stream.sender.OnSenderListener; 10 | import com.test.screenimage.stream.sender.Sender; 11 | import com.test.screenimage.stream.sender.sendqueue.ISendQueue; 12 | import com.test.screenimage.stream.sender.sendqueue.NormalSendQueue; 13 | import com.test.screenimage.stream.sender.sendqueue.SendQueueListener; 14 | import com.test.screenimage.stream.sender.tcp.interf.TcpConnectListener; 15 | import com.test.screenimage.utils.WeakHandler; 16 | 17 | 18 | /** 19 | * Created by wt 20 | * tcp发送 21 | */ 22 | 23 | public class TcpSender implements Sender, SendQueueListener { 24 | private ISendQueue mSendQueue = new NormalSendQueue(); 25 | private static final String TAG = "TcpSender"; 26 | private OnSenderListener sendListener; 27 | private TcpConnection mTcpConnection; 28 | private WeakHandler weakHandler = new WeakHandler(); 29 | private String ip; 30 | private int port; 31 | private int mainCmd; 32 | private int subCmd; 33 | //文本消息 34 | private String sendBody = null; 35 | 36 | 37 | public TcpSender(String ip, int port) { 38 | mTcpConnection = new TcpConnection(); 39 | this.ip = ip; 40 | this.port = port; 41 | } 42 | 43 | public void setVideoParams(VideoConfiguration videoConfiguration) { 44 | mTcpConnection.setVideoParams(videoConfiguration); 45 | } 46 | 47 | // TODO: 2018/6/11 wt设置主指令 48 | public void setMianCmd(int mainCmd) { 49 | this.mainCmd = mainCmd; 50 | } 51 | 52 | // TODO: 2018/6/11 wt设置子指令 53 | public void setSubCmd(int subCmd) { 54 | this.subCmd = subCmd; 55 | } 56 | 57 | // TODO: 2018/6/11 wt设置要发送的文本内容 58 | public void setSendBody(String body) { 59 | this.sendBody = body; 60 | } 61 | 62 | // TODO: 2018/5/29 wt 63 | @Override 64 | public void onData(byte[] data, int type) { 65 | Frame frame = null; 66 | if (type == TcpPacker.FIRST_VIDEO) { 67 | frame = new Frame(data, type, Frame.FRAME_TYPE_CONFIGURATION); 68 | } else if (type == TcpPacker.KEY_FRAME) { 69 | frame = new Frame(data, type, Frame.FRAME_TYPE_KEY_FRAME); 70 | } else if (type == TcpPacker.INTER_FRAME) { 71 | frame = new Frame(data, type, Frame.FRAME_TYPE_INTER_FRAME); 72 | } else if (type == TcpPacker.AUDIO) { 73 | frame = new Frame(data, type, Frame.FRAME_TYPE_AUDIO); 74 | } 75 | if (frame == null) { 76 | return; 77 | } 78 | mSendQueue.putFrame(frame); 79 | } 80 | 81 | /** 82 | * 开启连接 83 | */ 84 | public void openConnect() { 85 | //设置缓存队列 86 | mTcpConnection.setSendQueue(mSendQueue); 87 | new Thread(new Runnable() { 88 | @Override 89 | public void run() { 90 | connectNotInUi(); 91 | } 92 | }).start(); 93 | 94 | } 95 | 96 | @Override 97 | public void start() { 98 | mSendQueue.setSendQueueListener(this); 99 | mSendQueue.start(); 100 | } 101 | 102 | private synchronized void connectNotInUi() { 103 | //设置连接回调 104 | mTcpConnection.setConnectListener(mTcpListener); 105 | //开始连接服务器 106 | mTcpConnection.connect(ip, port, mainCmd, subCmd, sendBody); 107 | } 108 | 109 | // TODO: 2018/6/4 监听回调 110 | private TcpConnectListener mTcpListener = new TcpConnectListener() { 111 | @Override 112 | public void onSocketConnectSuccess() { 113 | // connected(); 114 | Log.e("wtt", "onSocketConnectSuccess"); 115 | } 116 | 117 | @Override 118 | public void onSocketConnectFail(String message) { 119 | Log.e("wtt", "onSocketConnectFail: zzzz"); 120 | weakHandler.post(new Runnable() { 121 | @Override 122 | public void run() { 123 | sendListener.onConnectFail(message); 124 | } 125 | }); 126 | } 127 | 128 | @Override 129 | public void onTcpConnectSuccess() { 130 | connected(); 131 | Log.e("wtt", "onTcpConnectSuccess"); 132 | } 133 | 134 | @Override 135 | public void onTcpConnectFail(String message) { 136 | Log.e("wtt", "onTcpConnectFail: zzz"); 137 | disConnected(message); 138 | } 139 | 140 | @Override 141 | public void onPublishSuccess() { 142 | //数据发送成功 143 | // connected(); 144 | } 145 | 146 | @Override 147 | public void onPublishFail() { 148 | //数据发送失败 149 | weakHandler.post(new Runnable() { 150 | @Override 151 | public void run() { 152 | sendListener.onPublishFail(); 153 | } 154 | }); 155 | } 156 | 157 | @Override 158 | public void onSocketDisconnect(String message) { 159 | Log.e("wtt", "onSocketDisconnect: xxxx"); 160 | disConnected(message); 161 | } 162 | 163 | @Override 164 | public void onNetSpeedChange(String msg) { 165 | weakHandler.post(new Runnable() { 166 | @Override 167 | public void run() { 168 | if (sendListener != null) sendListener.netSpeedChange(msg); 169 | } 170 | }); 171 | } 172 | 173 | }; 174 | 175 | 176 | @Override 177 | public void stop() { 178 | mTcpConnection.stop(); 179 | mSendQueue.stop(); 180 | } 181 | 182 | 183 | @Override 184 | public void good() { 185 | weakHandler.post(new Runnable() { 186 | @Override 187 | public void run() { 188 | //网络好 189 | sendListener.onNetGood(); 190 | } 191 | }); 192 | } 193 | 194 | @Override 195 | public void bad() { 196 | weakHandler.post(new Runnable() { 197 | @Override 198 | public void run() { 199 | //网络差 200 | sendListener.onNetBad(); 201 | } 202 | }); 203 | } 204 | 205 | 206 | // TODO: 2018/6/6 连接成功 207 | private void connected() { 208 | weakHandler.post(new Runnable() { 209 | @Override 210 | public void run() { 211 | sendListener.onConnected(); 212 | } 213 | }); 214 | } 215 | 216 | 217 | // TODO: 2018/6/6 连接失败 218 | private void disConnected(String message) { 219 | weakHandler.post(new Runnable() { 220 | @Override 221 | public void run() { 222 | sendListener.onDisConnected(message); 223 | } 224 | }); 225 | } 226 | 227 | public void setSenderListener(OnSenderListener listener) { 228 | this.sendListener = listener; 229 | } 230 | 231 | /** 232 | * add by wt 为解决首次黑屏而加 233 | */ 234 | public void setSpsPps(byte[] spsPps) { 235 | if (mTcpConnection != null) mTcpConnection.setSpsPps(spsPps); 236 | } 237 | 238 | } 239 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/tcp/TcpWriteThread.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.tcp; 2 | 3 | import android.text.TextUtils; 4 | import android.util.Log; 5 | 6 | import com.test.screenimage.entity.Frame; 7 | import com.test.screenimage.stream.sender.sendqueue.ISendQueue; 8 | import com.test.screenimage.stream.sender.tcp.interf.OnTcpWriteListener; 9 | 10 | import java.io.BufferedOutputStream; 11 | import java.io.IOException; 12 | import java.util.Timer; 13 | import java.util.TimerTask; 14 | 15 | /** 16 | * Created by wt 17 | * Date on 2018/5/28 18 | * 19 | * @Desc 20 | */ 21 | 22 | public class TcpWriteThread extends Thread { 23 | private BufferedOutputStream bos; 24 | private ISendQueue iSendQueue; 25 | private volatile boolean startFlag; 26 | private int mainCmd; 27 | private int subCmd; 28 | private String sendBody; 29 | private OnTcpWriteListener mListener; 30 | private final String TAG = "TcpWriteThread"; 31 | private volatile int readLength = 0; 32 | private Timer timer; 33 | private boolean isCalculate = false; 34 | 35 | /** 36 | * by wt 37 | * 38 | * @param bos 输入流 39 | * @param sendQueue 40 | * @param mainCmd 主指令 41 | * @param subCmd 子指令 42 | * @param sendBody 文本消息内容 43 | */ 44 | public TcpWriteThread(BufferedOutputStream bos, ISendQueue sendQueue, int mainCmd, 45 | int subCmd, String sendBody) { 46 | this.bos = bos; 47 | this.iSendQueue = sendQueue; 48 | this.mainCmd = mainCmd; 49 | this.subCmd = subCmd; 50 | this.sendBody = sendBody; 51 | startFlag = true; 52 | } 53 | 54 | public void setOnTcpWriteThread(OnTcpWriteListener listener) { 55 | this.mListener = listener; 56 | } 57 | 58 | @Override 59 | public void run() { 60 | super.run(); 61 | startNetSpeedCalculate(); 62 | while (startFlag) { 63 | Frame frame = iSendQueue.takeFrame(); 64 | if (frame == null) { 65 | continue; 66 | } 67 | // TODO: 2018/5/29 wt修改 68 | if (frame.data.length != 0) { 69 | sendData(frame.data); 70 | } 71 | } 72 | } 73 | 74 | // TODO: 2018/6/4 wt 发送数据 75 | public void sendData(byte[] buff) { 76 | try { 77 | byte[] sendBuff = new EncodeV1(mainCmd, subCmd, sendBody, buff).buildSendContent(); 78 | if (isCalculate) readLength += sendBuff.length; 79 | bos.write(sendBuff); 80 | bos.flush(); 81 | // Log.e(TAG,"send data "); 82 | } catch (IOException e) { 83 | startFlag = false; 84 | if (mListener != null) mListener.socketDisconnect(e.getMessage()); 85 | } 86 | } 87 | 88 | /** 89 | * 停止写 90 | */ 91 | public void shutDown() { 92 | mListener = null; 93 | isCalculate = false; 94 | try { 95 | if (timer != null) timer.cancel(); 96 | } catch (Exception e) { 97 | } 98 | startFlag = false; 99 | this.interrupt(); 100 | } 101 | 102 | public void sendStartBuff() { 103 | sendData(new byte[0]); 104 | } 105 | 106 | public void startNetSpeedCalculate() { 107 | try { 108 | if (timer != null) timer.cancel(); 109 | } catch (Exception e) { 110 | } 111 | readLength = 0; 112 | isCalculate = true; 113 | timer = new Timer(); 114 | timer.schedule(new TimerTask() { 115 | @Override 116 | public void run() { 117 | if (mListener != null) { 118 | mListener.netSpeedChange((readLength / 1024) + " kb/s"); 119 | readLength = 0; 120 | } 121 | } 122 | }, 1000, 1000); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/tcp/interf/OnTcpReadListener.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.tcp.interf; 2 | 3 | import com.test.screenimage.entity.ReceiveData; 4 | import com.test.screenimage.entity.ReceiveHeader; 5 | 6 | /** 7 | * Created by wt 8 | * Date on 2018/5/28 9 | * 10 | * @Desc 从tcp read thread中的回调 11 | */ 12 | 13 | public interface OnTcpReadListener { 14 | 15 | void socketDisconnect(String message); //断开连接 16 | 17 | //收到server消息,连接成功 18 | //date:解析后的数据包 19 | void connectSuccess(ReceiveData data); 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/tcp/interf/OnTcpWriteListener.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.tcp.interf; 2 | 3 | /** 4 | * Created by wt 5 | * Date on 2018/5/28 6 | * 从tcp Write thread中的回调 7 | */ 8 | 9 | public interface OnTcpWriteListener { 10 | //断开连接 11 | void socketDisconnect(String message); 12 | 13 | void netSpeedChange(String msg); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/tcp/interf/TcpConnectListener.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.tcp.interf; 2 | 3 | /** 4 | * Created by wt 5 | * Date on 2018/5/28 6 | * 7 | * @Desc 连接监听 8 | */ 9 | 10 | public interface TcpConnectListener { 11 | //socket连接成功 12 | void onSocketConnectSuccess(); 13 | 14 | //socket连接失败 15 | void onSocketConnectFail(String message); 16 | 17 | //tcp连接成功 18 | void onTcpConnectSuccess(); 19 | 20 | //tcp连接失败 21 | void onTcpConnectFail(String message); 22 | 23 | //发送成功 24 | void onPublishSuccess(); 25 | 26 | //发送失败 27 | void onPublishFail(); 28 | 29 | //socket断开连接 30 | void onSocketDisconnect(String message); 31 | 32 | void onNetSpeedChange(String msg); 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/udp/UDPClientThread.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.udp; 2 | 3 | import android.os.Message; 4 | import android.util.Log; 5 | 6 | import com.test.screenimage.MyApplication; 7 | import com.test.screenimage.stream.sender.udp.interf.OnUdpConnectListener; 8 | 9 | import java.io.IOException; 10 | import java.net.DatagramPacket; 11 | import java.net.InetAddress; 12 | import java.net.MulticastSocket; 13 | 14 | /** 15 | * Created by wt on 2018/7/11. 16 | * 基于udp的组网连接 17 | */ 18 | public class UDPClientThread extends Thread { 19 | static final String BROADCAST_IP = "224.0.0.1"; 20 | //监听的端口号 21 | static final int BROADCAST_PORT = 15000; 22 | private InetAddress inetAddress = null; 23 | //服务端的局域网IP 24 | private static String ip; 25 | private OnUdpConnectListener mListener; 26 | 27 | public UDPClientThread(OnUdpConnectListener listener) { 28 | this.mListener = listener; 29 | this.start(); 30 | } 31 | 32 | @Override 33 | public void run() { 34 | MulticastSocket multicastSocket = null;//多点广播套接字 35 | try { 36 | /** 37 | * 1.实例化MulticastSocket对象,并指定端口 38 | * 2.加入广播地址,MulticastSocket使用public void joinGroup(InetAddress mcastaddr) 39 | * 3.开始接收广播 40 | * 4.关闭广播 41 | */ 42 | multicastSocket = new MulticastSocket(BROADCAST_PORT); 43 | inetAddress = InetAddress.getByName(BROADCAST_IP); 44 | Log.e("UdpClientThread", "udp server start"); 45 | multicastSocket.joinGroup(inetAddress); 46 | byte buf[] = new byte[1024]; 47 | DatagramPacket dp = new DatagramPacket(buf, buf.length); 48 | while (true) { 49 | multicastSocket.receive(dp); 50 | Log.e("UdpClientThread", "receive a msg"); 51 | ip = new String(buf, 0, dp.getLength()); 52 | multicastSocket.leaveGroup(inetAddress); 53 | multicastSocket.close(); 54 | MyApplication.mHandler.post(new Runnable() { 55 | @Override 56 | public void run() { 57 | mListener.udpConnectSuccess(ip); 58 | } 59 | }); 60 | } 61 | } catch (Exception e) { 62 | MyApplication.mHandler.post(new Runnable() { 63 | @Override 64 | public void run() { 65 | mListener.udpDisConnec(e.getMessage()); 66 | } 67 | }); 68 | } finally { 69 | Log.e("UdpClientThread", "udp server close"); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/stream/sender/udp/interf/OnUdpConnectListener.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.stream.sender.udp.interf; 2 | 3 | /** 4 | * Created by wt on 2018/7/11. 5 | */ 6 | public interface OnUdpConnectListener { 7 | void udpConnectSuccess(String ip); 8 | void udpDisConnec(String message); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/AnalyticDataUtils.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | import android.util.Log; 4 | 5 | import com.test.screenimage.entity.ReceiveData; 6 | import com.test.screenimage.entity.ReceiveHeader; 7 | 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | 12 | /** 13 | * Created by wt on 2018/6/12. 14 | * 根据协议解析数据 15 | */ 16 | public class AnalyticDataUtils { 17 | private OnAnalyticDataListener mListener; 18 | 19 | // TODO: 2018/6/11 wt处理协议相应指令,把消息内容解析出来 20 | public void analyticData(InputStream is, ReceiveHeader receiveHeader) throws IOException { 21 | byte[] sendBody = null; 22 | byte[] buff = null; 23 | //文本长度 24 | if (receiveHeader.getStringBodylength() != 0) { 25 | sendBody = readByte(is, receiveHeader.getStringBodylength()); 26 | } 27 | //音视频长度 28 | if (receiveHeader.getBuffSize() != 0) { 29 | buff = readByte(is, receiveHeader.getBuffSize()); 30 | } 31 | ReceiveData data = new ReceiveData(); 32 | data.setHeader(receiveHeader); 33 | data.setSendBody(sendBody == null ? "" : new String(sendBody)); 34 | data.setBuff(buff); 35 | if (mListener != null) mListener.onSuccess(data); 36 | } 37 | 38 | 39 | /** 40 | * 保证从流里读到指定长度数据 41 | * 42 | * @param is 43 | * @param readSize 44 | * @return 45 | * @throws IOException 46 | */ 47 | public byte[] readByte(InputStream is, int readSize) throws IOException { 48 | byte[] buff = new byte[readSize]; 49 | int len = 0; 50 | int eachLen = 0; 51 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 52 | while (len < readSize) { 53 | eachLen = is.read(buff); 54 | if (eachLen != -1) { 55 | len += eachLen; 56 | baos.write(buff, 0, eachLen); 57 | } else { 58 | baos.close(); 59 | throw new IOException(); 60 | } 61 | if (len < readSize) { 62 | buff = new byte[readSize - len]; 63 | } 64 | } 65 | byte[] b = baos.toByteArray(); 66 | baos.close(); 67 | return b; 68 | } 69 | /** 70 | * 实现数组之间的复制,分析数据头 71 | * bytes:源数组 72 | * srcPos:源数组要复制的起始位置 73 | * dest:目的数组 74 | * destPos:目的数组放置的起始位置 75 | * length:复制的长度 76 | */ 77 | public ReceiveHeader analysisHeader(byte[] header) { 78 | byte[] buff = new byte[4]; 79 | System.arraycopy(header, 1, buff, 0, 4); 80 | final short mainCmd = ByteUtil.bytesToShort(buff); //主指令 1`5 81 | buff = new byte[4]; 82 | System.arraycopy(header, 5, buff, 0, 4); 83 | final short subCmd = ByteUtil.bytesToShort(buff); //子指令 5`9 84 | buff = new byte[4]; 85 | System.arraycopy(header, 9, buff, 0, 4); 86 | int stringBodyLength = ByteUtil.bytesToInt(buff);//文本数据 9 ~ 13; 87 | buff = new byte[4]; 88 | System.arraycopy(header, 13, buff, 0, 4); 89 | int byteBodySize = ByteUtil.bytesToInt(buff);//byte数据 13^17 90 | return new ReceiveHeader(mainCmd, subCmd, header[0], stringBodyLength, byteBodySize); 91 | } 92 | 93 | /** 94 | * 分离解析头数据 95 | * @param is 96 | * @param receiveHeader 97 | * @return 98 | * @throws IOException 99 | */ 100 | public ReceiveData synchAnalyticData(InputStream is, ReceiveHeader receiveHeader) throws IOException { 101 | byte[] sendBody = null; 102 | byte[] buff = null; 103 | //文本长度 104 | if (receiveHeader.getStringBodylength() != 0) { 105 | sendBody = readByte(is, receiveHeader.getStringBodylength()); 106 | } 107 | //音视频长度 108 | if (receiveHeader.getBuffSize() != 0) { 109 | buff = readByte(is, receiveHeader.getBuffSize()); 110 | } 111 | ReceiveData data = new ReceiveData(); 112 | data.setHeader(receiveHeader); 113 | data.setSendBody(sendBody == null ? "" : new String(sendBody)); 114 | Log.e("wtt", "analyticData: " + new String(sendBody)); 115 | data.setBuff(buff); 116 | return data; 117 | } 118 | // TODO: 2018/6/12 wt回调解析后数据 119 | public interface OnAnalyticDataListener { 120 | void onSuccess(ReceiveData data); 121 | 122 | } 123 | 124 | public void setOnAnalyticDataListener(OnAnalyticDataListener listener) { 125 | this.mListener = listener; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/AudioUtils.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.media.AudioFormat; 6 | import android.media.AudioManager; 7 | import android.media.AudioRecord; 8 | import android.media.MediaRecorder; 9 | import android.util.Log; 10 | 11 | import com.test.screenimage.configuration.AudioConfiguration; 12 | 13 | 14 | public class AudioUtils { 15 | // public static boolean checkMicSupport(AudioConfiguration audioConfiguration) { 16 | // boolean result; 17 | // int recordBufferSize = getRecordBufferSize(audioConfiguration); 18 | // byte[] mRecordBuffer = new byte[recordBufferSize]; 19 | // AudioRecord audioRecord = getAudioRecord(audioConfiguration); 20 | // try { 21 | // audioRecord.startRecording(); 22 | // } catch (Exception e) { 23 | // e.printStackTrace(); 24 | // } 25 | // int readLen = audioRecord.read(mRecordBuffer, 0, recordBufferSize); 26 | // result = readLen >= 0; 27 | // try { 28 | // audioRecord.release(); 29 | // } catch (Exception e) { 30 | // e.printStackTrace(); 31 | // } 32 | // return result; 33 | // } 34 | 35 | 36 | @TargetApi(18) 37 | public static AudioRecord getAudioRecord(AudioConfiguration audioConfiguration,Context context) 38 | { 39 | int frequency = audioConfiguration.frequency; 40 | int audioEncoding = audioConfiguration.encoding; 41 | //单声道(定义采样通道) 42 | int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO; 43 | if(audioConfiguration.channelCount == 2) { 44 | channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_STEREO; 45 | } 46 | //音频源:麦克风 47 | int audioSource = MediaRecorder.AudioSource.MIC; 48 | if(audioConfiguration.aec) { 49 | //对麦克风中类似ip通话的交流声音进行识别,默认会开启回声消除和自动增益 50 | audioSource = MediaRecorder.AudioSource.VOICE_COMMUNICATION; 51 | } 52 | // TODO: 2018/6/5 wt 消除回声 53 | AudioManager audioManager = (AudioManager)context.getSystemService(context.AUDIO_SERVICE); 54 | audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 55 | audioManager.setSpeakerphoneOn(false); 56 | audioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, 0, 57 | AudioManager.STREAM_VOICE_CALL); 58 | //音频源,采样率,声道数,采样位数,缓冲区大小 59 | Log.e("wtt", "getAudioRecord: "+ getRecordBufferSize(audioConfiguration) ); 60 | AudioRecord audioRecord = new AudioRecord(audioSource, frequency, 61 | channelConfiguration, audioEncoding, getRecordBufferSize(audioConfiguration)); 62 | return audioRecord; 63 | } 64 | 65 | // TODO: 2018/7/16 wt 内部的音频缓冲区的大小 66 | public static int getRecordBufferSize(AudioConfiguration audioConfiguration) { 67 | int frequency = audioConfiguration.frequency; 68 | int audioEncoding = audioConfiguration.encoding; 69 | int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO; 70 | if(audioConfiguration.channelCount == 2) { 71 | channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_STEREO; 72 | } 73 | int size = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding); 74 | return size; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/BatteryUtils.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.os.Build; 7 | import android.os.PowerManager; 8 | import android.provider.Settings; 9 | import android.support.annotation.RequiresApi; 10 | 11 | import static android.content.Context.POWER_SERVICE; 12 | 13 | /** 14 | * Created by wt 15 | * Date on 2017/12/13 15:20:46. 16 | * 17 | * @Desc 检查设置忽略电池优化. 18 | */ 19 | 20 | public class BatteryUtils { 21 | 22 | @RequiresApi(api = Build.VERSION_CODES.M) 23 | public static void ignoreBatteryOptimization(Context context) { 24 | PowerManager powerManager = (PowerManager) context.getApplicationContext().getSystemService(POWER_SERVICE); 25 | boolean hasIgnored = powerManager.isIgnoringBatteryOptimizations(context.getPackageName()); 26 | // 判断当前APP是否有加入电池优化的白名单,如果没有,弹出加入电池优化的白名单的设置对话框。 27 | if (!hasIgnored) { 28 | Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); 29 | intent.setData(Uri.parse("package:" + context.getPackageName())); 30 | context.startActivity(intent); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/BlackListHelper.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | import android.os.Build; 4 | import android.text.TextUtils; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | 10 | 11 | public class BlackListHelper { 12 | private static final String[] BLACKLISTED_AEC_MODELS = new String[] { 13 | "Nexus 5", // Nexus 5 14 | }; 15 | 16 | private static final String[] BLACKLISTED_FPS_MODELS = new String[] { 17 | "OPPO R9", 18 | "Nexus 6P", 19 | }; 20 | 21 | 22 | public static boolean deviceInAecBlacklisted() { 23 | List blackListedModels = Arrays.asList(BLACKLISTED_AEC_MODELS); 24 | for(String blackModel : blackListedModels) { 25 | String model = Build.MODEL; 26 | if(!TextUtils.isEmpty(model) && model.contains(blackModel)) { 27 | return true; 28 | } 29 | } 30 | return false; 31 | } 32 | 33 | public static boolean deviceInFpsBlacklisted() { 34 | List blackListedModels = Arrays.asList(BLACKLISTED_FPS_MODELS); 35 | for(String blackModel : blackListedModels) { 36 | String model = Build.MODEL; 37 | if(!TextUtils.isEmpty(model) && model.contains(blackModel)) { 38 | return true; 39 | } 40 | } 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/ByteUtil.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | /** 4 | * Created by wt 5 | * Date on 2018/5/28 6 | */ 7 | public class ByteUtil { 8 | /** 9 | * 将int转为长度为4的byte数组 10 | * 11 | * @param length 12 | * @return 13 | */ 14 | public static byte[] int2Bytes(int length) { 15 | byte[] result = new byte[4]; 16 | result[0] = (byte) length; 17 | result[1] = (byte) (length >> 8); 18 | result[2] = (byte) (length >> 16); 19 | result[3] = (byte) (length >> 24); 20 | return result; 21 | } 22 | 23 | //转成2个字节 24 | public static byte[] short2Bytes(short size) { 25 | byte[] result = new byte[2]; 26 | result[0] = (byte) size; 27 | result[1] = (byte) (size >> 8); 28 | return result; 29 | } 30 | /** 31 | * byte数组中取int数值,本方法适用于(低位在前,高位在后)的顺序,和和intToBytes()配套使用 32 | * 33 | * @param src byte数组 34 | * @return int数值 35 | */ 36 | public static int bytesToInt(byte[] src) { 37 | int value; 38 | value = (int) ((src[0] & 0xFF) 39 | | ((src[1] & 0xFF) << 8) 40 | | ((src[2] & 0xFF) << 16) 41 | | ((src[3] & 0xFF) << 24)); 42 | return value; 43 | } 44 | 45 | // TODO: 2018/6/11 wt byte转short 46 | public static short bytesToShort(byte[] src) { 47 | short value; 48 | value = (short) ((src[0] & 0xFF) 49 | | ((src[1] & 0xFF) << 8)); 50 | return value; 51 | } 52 | 53 | 54 | /** 55 | * 获得校验码 56 | * 57 | * @param bytes 根据通讯协议的前12个字节 58 | * @return 59 | */ 60 | public static byte getCheckCode(byte[] bytes) { 61 | byte b = 0x00; 62 | for (int i = 0; i < bytes.length; i++) { 63 | b ^= bytes[i]; 64 | } 65 | return b; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/DialogUtils.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Dialog; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.support.v7.app.AlertDialog; 8 | import android.content.DialogInterface.OnClickListener; 9 | import android.view.View; 10 | 11 | /** 12 | * Created by wt on 2018/6/25. 13 | * 弹出框工具类 14 | */ 15 | public class DialogUtils { 16 | 17 | private static DialogUtils instance = null; 18 | 19 | public static DialogUtils getInstance() { 20 | if (instance == null) { 21 | instance = new DialogUtils(); 22 | } 23 | return instance; 24 | } 25 | 26 | public void showDialog(Context context, String titleInfo,String message,String postBtnName, 27 | String navBtnName, 28 | final DialogUtils.DialogCallBack callBack) { 29 | AlertDialog.Builder alterDialog = new AlertDialog.Builder(context); 30 | alterDialog.setTitle(titleInfo); 31 | alterDialog.setMessage(message); 32 | alterDialog.setCancelable(true); 33 | 34 | alterDialog.setPositiveButton(postBtnName, new DialogInterface.OnClickListener() { 35 | @Override 36 | public void onClick(DialogInterface dialog, int which) { 37 | callBack.exectEvent(); 38 | } 39 | }); 40 | alterDialog.setNegativeButton(navBtnName, new DialogInterface.OnClickListener() { 41 | @Override 42 | public void onClick(DialogInterface dialog, int which) { 43 | dialog.cancel(); 44 | } 45 | }); 46 | alterDialog.show(); 47 | } 48 | 49 | public interface DialogCallBack { 50 | void exectEvent(); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/SopCastLog.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * 打印log 7 | */ 8 | public class SopCastLog { 9 | private static boolean open = true; 10 | 11 | public static void isOpen(boolean isOpen) { 12 | open = isOpen; 13 | } 14 | 15 | public static void d(String tag, String msg) { 16 | if(open) { 17 | Log.d(tag, msg); 18 | } 19 | } 20 | 21 | public static void w(String tag, String msg) { 22 | if(open) { 23 | Log.w(tag, msg); 24 | } 25 | } 26 | 27 | public static void e(String tag, String msg) { 28 | if(open) { 29 | Log.e(tag, msg); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/SopCastUtils.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | import android.os.Build; 4 | import android.os.Looper; 5 | 6 | 7 | public class SopCastUtils { 8 | 9 | public interface INotUIProcessor { 10 | void process(); 11 | } 12 | 13 | public static void processNotUI(final INotUIProcessor processor) { 14 | if(Looper.myLooper() == Looper.getMainLooper()) { 15 | new Thread(new Runnable() { 16 | @Override 17 | public void run() { 18 | processor.process(); 19 | } 20 | }).start(); 21 | } else { 22 | processor.process(); 23 | } 24 | } 25 | 26 | public static boolean isOverLOLLIPOP() { 27 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/StatusBarUtil.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.graphics.Color; 6 | import android.os.Build; 7 | import android.view.View; 8 | import android.view.Window; 9 | import android.view.WindowManager; 10 | 11 | import java.lang.reflect.Field; 12 | import java.lang.reflect.Method; 13 | 14 | /** 15 | * Created by wt on 2017/4/22. 16 | */ 17 | 18 | public class StatusBarUtil { 19 | /** 20 | * 修改状态栏为全透明 21 | * @param activity 22 | */ 23 | @TargetApi(19) 24 | public static void transparencyBar(Activity activity){ 25 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 26 | Window window = activity.getWindow(); 27 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 28 | window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 29 | | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 30 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 31 | window.setStatusBarColor(Color.TRANSPARENT); 32 | 33 | } else 34 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 35 | Window window =activity.getWindow(); 36 | window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, 37 | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 38 | } 39 | } 40 | 41 | 42 | 43 | /** 44 | *设置状态栏黑色字体图标, 45 | * 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android 46 | * @param activity 47 | * @return 1:MIUUI 2:Flyme 3:android6.0 48 | */ 49 | public static int StatusBarLightMode(Activity activity){ 50 | int result=0; 51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 52 | if(MIUISetStatusBarLightMode(activity.getWindow(), true)){ 53 | result=1; 54 | }else if(FlymeSetStatusBarLightMode(activity.getWindow(), true)){ 55 | result=2; 56 | }else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 57 | activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); 58 | result=3; 59 | } 60 | } 61 | return result; 62 | } 63 | 64 | /** 65 | * 已知系统类型时,设置状态栏黑色字体图标。 66 | * 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android 67 | * @param activity 68 | * @param type 1:MIUUI 2:Flyme 3:android6.0 69 | */ 70 | public static void StatusBarLightMode(Activity activity,int type){ 71 | if(type==1){ 72 | MIUISetStatusBarLightMode(activity.getWindow(), true); 73 | }else if(type==2){ 74 | FlymeSetStatusBarLightMode(activity.getWindow(), true); 75 | }else if(type==3){ 76 | activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); 77 | } 78 | 79 | } 80 | 81 | /** 82 | * 清除MIUI或flyme或6.0以上版本状态栏黑色字体 83 | */ 84 | public static void StatusBarDarkMode(Activity activity,int type){ 85 | if(type==1){ 86 | MIUISetStatusBarLightMode(activity.getWindow(), false); 87 | }else if(type==2){ 88 | FlymeSetStatusBarLightMode(activity.getWindow(), false); 89 | }else if(type==3){ 90 | activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); 91 | } 92 | 93 | } 94 | 95 | 96 | /** 97 | * 设置状态栏图标为深色和魅族特定的文字风格 98 | * 可以用来判断是否为Flyme用户 99 | * @param window 需要设置的窗口 100 | * @param dark 是否把状态栏字体及图标颜色设置为深色 101 | * @return boolean 成功执行返回true 102 | * 103 | */ 104 | public static boolean FlymeSetStatusBarLightMode(Window window, boolean dark) { 105 | boolean result = false; 106 | if (window != null) { 107 | try { 108 | WindowManager.LayoutParams lp = window.getAttributes(); 109 | Field darkFlag = WindowManager.LayoutParams.class 110 | .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON"); 111 | Field meizuFlags = WindowManager.LayoutParams.class 112 | .getDeclaredField("meizuFlags"); 113 | darkFlag.setAccessible(true); 114 | meizuFlags.setAccessible(true); 115 | int bit = darkFlag.getInt(null); 116 | int value = meizuFlags.getInt(lp); 117 | if (dark) { 118 | value |= bit; 119 | } else { 120 | value &= ~bit; 121 | } 122 | meizuFlags.setInt(lp, value); 123 | window.setAttributes(lp); 124 | result = true; 125 | } catch (Exception e) { 126 | 127 | } 128 | } 129 | return result; 130 | } 131 | 132 | /** 133 | * 设置状态栏字体图标为深色,需要MIUIV6以上 134 | * @param window 需要设置的窗口 135 | * @param dark 是否把状态栏字体及图标颜色设置为深色 136 | * @return boolean 成功执行返回true 137 | * 138 | */ 139 | public static boolean MIUISetStatusBarLightMode(Window window, boolean dark) { 140 | boolean result = false; 141 | if (window != null) { 142 | Class clazz = window.getClass(); 143 | try { 144 | int darkModeFlag = 0; 145 | Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams"); 146 | Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE"); 147 | darkModeFlag = field.getInt(layoutParams); 148 | Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class); 149 | if(dark){ 150 | extraFlagField.invoke(window,darkModeFlag,darkModeFlag);//状态栏透明且黑色字体 151 | }else{ 152 | extraFlagField.invoke(window, 0, darkModeFlag);//清除黑色字体 153 | } 154 | result=true; 155 | }catch (Exception e){ 156 | 157 | } 158 | } 159 | return result; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/SupportMultipleScreensUtil.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.graphics.drawable.Drawable; 6 | import android.util.DisplayMetrics; 7 | import android.util.Log; 8 | import android.util.TypedValue; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.view.ViewGroup.LayoutParams; 12 | import android.view.ViewGroup.MarginLayoutParams; 13 | import android.widget.TextView; 14 | 15 | import com.test.screenimage.R; 16 | 17 | 18 | /** 19 | * 关于适配 20 | */ 21 | public class SupportMultipleScreensUtil { 22 | 23 | public static final int BASE_SCREEN_WIDTH = 1080; 24 | public static final int BASE_SCREEN_HEIGHT = 1920; 25 | public static final float BASE_SCREEN_WIDTH_FLOAT = 1080F; 26 | public static final float BASE_SCREEN_HEIGHT_FLOAT = 1920F; 27 | public static float scale = 1.0F; 28 | 29 | public SupportMultipleScreensUtil() { 30 | 31 | } 32 | 33 | public static void init(Context context) { 34 | Resources resources=context.getResources(); 35 | DisplayMetrics displayMetrics = resources.getDisplayMetrics(); 36 | int widthPixels = displayMetrics.widthPixels; 37 | Log.e("123", "init: "+widthPixels+"<<<<<<<"+displayMetrics.heightPixels ); 38 | scale = (float)widthPixels / BASE_SCREEN_WIDTH_FLOAT; 39 | } 40 | 41 | 42 | public static void scale(View view) { 43 | if(null != view) { 44 | if(view instanceof ViewGroup) { 45 | scaleViewGroup((ViewGroup)view); 46 | } else { 47 | scaleView(view); 48 | } 49 | } 50 | } 51 | 52 | private static void scaleView(View view) { 53 | Object isScale = view.getTag(R.id.is_scale_size_tag); 54 | if (!(isScale instanceof Boolean) || !((Boolean) isScale).booleanValue()) { 55 | if (view instanceof TextView) { 56 | scaleTextView((TextView) view); 57 | } else { 58 | scaleViewSize(view); 59 | } 60 | view.setTag(R.id.is_scale_size_tag, Boolean.valueOf(true)); 61 | } 62 | } 63 | 64 | 65 | 66 | private static void scaleViewGroup(ViewGroup viewGroup) { 67 | for (int i = 0; i < viewGroup.getChildCount(); ++i) { 68 | View view = viewGroup.getChildAt(i); 69 | if (view instanceof ViewGroup) { 70 | scaleViewGroup((ViewGroup) view); 71 | } 72 | scaleView(view); 73 | } 74 | } 75 | 76 | public static void scaleViewSize(View view) { 77 | if (null != view) { 78 | int paddingLeft = getScaleValue(view.getPaddingLeft()); 79 | int paddingTop = getScaleValue(view.getPaddingTop()); 80 | int paddingRight = getScaleValue(view.getPaddingRight()); 81 | int paddingBottom = getScaleValue(view.getPaddingBottom()); 82 | view.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); 83 | 84 | LayoutParams layoutParams = view.getLayoutParams(); 85 | 86 | if (null != layoutParams) { 87 | 88 | if (layoutParams.width > 0) { 89 | layoutParams.width = getScaleValue(layoutParams.width); 90 | } 91 | 92 | if (layoutParams.height > 0) { 93 | layoutParams.height = getScaleValue(layoutParams.height); 94 | } 95 | 96 | if (layoutParams instanceof MarginLayoutParams) { 97 | MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams; 98 | int topMargin = getScaleValue(marginLayoutParams.topMargin); 99 | int leftMargin = getScaleValue(marginLayoutParams.leftMargin); 100 | int bottomMargin = getScaleValue(marginLayoutParams.bottomMargin); 101 | int rightMargin = getScaleValue(marginLayoutParams.rightMargin); 102 | marginLayoutParams.topMargin = topMargin; 103 | marginLayoutParams.leftMargin = leftMargin; 104 | marginLayoutParams.bottomMargin = bottomMargin; 105 | marginLayoutParams.rightMargin = rightMargin; 106 | } 107 | } 108 | view.setLayoutParams(layoutParams); 109 | } 110 | } 111 | 112 | private static void setTextViewCompoundDrawables(TextView textView, Drawable leftDrawable, Drawable topDrawable, Drawable rightDrawable, Drawable bottomDrawable) { 113 | if(null != leftDrawable) { 114 | scaleDrawableBounds(leftDrawable); 115 | } 116 | 117 | if(null != rightDrawable) { 118 | scaleDrawableBounds(rightDrawable); 119 | } 120 | 121 | if(null != topDrawable) { 122 | scaleDrawableBounds(topDrawable); 123 | } 124 | 125 | if(null != bottomDrawable) { 126 | scaleDrawableBounds(bottomDrawable); 127 | } 128 | 129 | textView.setCompoundDrawables(leftDrawable, topDrawable, rightDrawable, bottomDrawable); 130 | } 131 | 132 | public static Drawable scaleDrawableBounds(Drawable drawable) { 133 | int right=getScaleValue(drawable.getIntrinsicWidth()); 134 | int bottom=getScaleValue(drawable.getIntrinsicHeight()); 135 | drawable.setBounds(0, 0, right, bottom); 136 | return drawable; 137 | } 138 | 139 | public static void scaleTextView(TextView textView) { 140 | if (null != textView) { 141 | 142 | scaleViewSize(textView); 143 | 144 | Object isScale = textView.getTag(R.id.is_scale_font_tag); 145 | if (!(isScale instanceof Boolean) || !((Boolean) isScale).booleanValue()) { 146 | float size = textView.getTextSize(); 147 | size *= scale; 148 | textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); 149 | } 150 | 151 | Drawable[] drawables = textView.getCompoundDrawables(); 152 | Drawable leftDrawable = drawables[0]; 153 | Drawable topDrawable = drawables[1]; 154 | Drawable rightDrawable = drawables[2]; 155 | Drawable bottomDrawable = drawables[3]; 156 | setTextViewCompoundDrawables(textView, leftDrawable, topDrawable, rightDrawable, bottomDrawable); 157 | int compoundDrawablePadding = getScaleValue(textView.getCompoundDrawablePadding()); 158 | textView.setCompoundDrawablePadding(compoundDrawablePadding); 159 | } 160 | } 161 | 162 | public static int getScaleValue(int value) { 163 | return value <= 2?value:(int)Math.ceil((double)(scale * (float)value)); 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/ToastUtils.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | /** 4 | * Created by wt on 2016/6/20. 5 | */ 6 | import android.content.Context; 7 | import android.text.TextUtils; 8 | import android.widget.Toast; 9 | 10 | public class ToastUtils 11 | { 12 | public static boolean isShow = true; 13 | private static Toast mToast; 14 | 15 | public static void show(Context paramContext, int paramInt1, int paramInt2) 16 | { 17 | if (isShow) 18 | Toast.makeText(paramContext, paramInt1, paramInt2).show(); 19 | } 20 | 21 | public static void show(Context paramContext, CharSequence paramCharSequence, int paramInt) 22 | { 23 | if (isShow) 24 | Toast.makeText(paramContext, paramCharSequence, paramInt).show(); 25 | } 26 | 27 | public static void showLong(Context paramContext, int paramInt) 28 | { 29 | if (isShow) 30 | Toast.makeText(paramContext, paramInt, Toast.LENGTH_LONG).show(); 31 | } 32 | 33 | public static void showLong(Context paramContext, CharSequence paramCharSequence) 34 | { 35 | if (isShow) 36 | Toast.makeText(paramContext, paramCharSequence, Toast.LENGTH_LONG).show(); 37 | } 38 | 39 | public static void showShort(Context paramContext, int paramInt) 40 | { 41 | if (isShow) 42 | Toast.makeText(paramContext, paramInt, Toast.LENGTH_SHORT).show(); 43 | } 44 | 45 | public static void showShort(Context paramContext, CharSequence paramCharSequence) 46 | { 47 | if (isShow) 48 | Toast.makeText(paramContext, paramCharSequence, Toast.LENGTH_SHORT).show(); 49 | } 50 | 51 | public static void showToast(Context paramContext, String paramString) 52 | { 53 | if ((TextUtils.isEmpty(paramString)) || (paramContext == null)) 54 | return; 55 | Toast.makeText(paramContext, paramString, Toast.LENGTH_SHORT).show(); 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/utils/WeakHandler.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.utils; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | import android.os.Message; 6 | 7 | import java.lang.ref.WeakReference; 8 | import java.util.concurrent.locks.Lock; 9 | import java.util.concurrent.locks.ReentrantLock; 10 | 11 | /** 12 | * 关于内存处理类 13 | * 在执行队列中,处理程序的原始实现始终保持对处理程序的硬引用。 14 | * 如果您创建匿名处理程序并将延迟消息插入其中,它将保持所有父类在记忆中的那个时间,即使它可以被清理。 15 | * 此实现更为棘手,它将对运行信息和消息保持弱引用,和GC可以收集它们一旦弱处理程序实例不再被引用。 16 | */ 17 | @SuppressWarnings("unused") 18 | public class WeakHandler { 19 | private final Handler.Callback mCallback; // hard reference to Callback. We need to keep callback in memory 20 | private final ExecHandler mExec; 21 | private Lock mLock = new ReentrantLock(); 22 | @SuppressWarnings("ConstantConditions") 23 | final ChainedRef mRunnables = new ChainedRef(mLock, null); 24 | 25 | /** 26 | * 如果该线程没有一个活套,这个处理程序将无法接收消息 27 | * 抛出异常。 28 | */ 29 | public WeakHandler() { 30 | mCallback = null; 31 | mExec = new ExecHandler(); 32 | } 33 | 34 | /** 35 | * 当前线程并采用回调接口,在该接口中可以处理消息 36 | * @param callback 回调接口,用于处理消息或空. 37 | */ 38 | public WeakHandler(Handler.Callback callback) { 39 | mCallback = callback; // Hard referencing body 40 | mExec = new ExecHandler(new WeakReference<>(callback)); // Weak referencing inside ExecHandler 41 | } 42 | 43 | /** 44 | * 使用所提供的{Link Looper-}而不是默认的。 45 | * 46 | * @param looper 套环,不能为空。 47 | */ 48 | public WeakHandler(Looper looper) { 49 | mCallback = null; 50 | mExec = new ExecHandler(looper); 51 | } 52 | 53 | /** 54 | * 调用回调处理消息的接口 55 | * 56 | * @param looper 57 | * @param callback 回调接口,用于处理消息或空 . 58 | */ 59 | public WeakHandler(Looper looper, Handler.Callback callback) { 60 | mCallback = callback; 61 | mExec = new ExecHandler(looper, new WeakReference<>(callback)); 62 | } 63 | 64 | /** 65 | * 使可运行R添加到消息队列中。 66 | * @param r 67 | * @return RealRealTrue如果RunnLabess成功放置到消息队列。在失败时返回false, 68 | * 通常是因为活套处理消息队列正在退出。 69 | */ 70 | public final boolean post(Runnable r) { 71 | return mExec.post(wrapRunnable(r)); 72 | } 73 | 74 | /** 75 | * 回调应该运行的绝对时间,使用时基,RealRealTrue如果RunnLabess成功放置到消息队列。 76 | * 在失败时返回false,通常是因为 77 | * 活套处理消息队列正在退出。注意真的结果并不意味着可运行的将被处理, 78 | * 在消息传递时间之前退出套接字发生,然后消息将被删除。 79 | */ 80 | public final boolean postAtTime(Runnable r, long uptimeMillis) { 81 | return mExec.postAtTime(wrapRunnable(r), uptimeMillis); 82 | } 83 | 84 | 85 | public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) { 86 | return mExec.postAtTime(wrapRunnable(r), token, uptimeMillis); 87 | } 88 | 89 | 90 | public final boolean postDelayed(Runnable r, long delayMillis) { 91 | return mExec.postDelayed(wrapRunnable(r), delayMillis); 92 | } 93 | 94 | 95 | public final boolean postAtFrontOfQueue(Runnable r) { 96 | return mExec.postAtFrontOfQueue(wrapRunnable(r)); 97 | } 98 | 99 | /** 100 | * 删除消息队列中的RunnR R的任何挂起的帖子 101 | */ 102 | public final void removeCallbacks(Runnable r) { 103 | final WeakRunnable runnable = mRunnables.remove(r); 104 | if (runnable != null) { 105 | mExec.removeCallbacks(runnable); 106 | } 107 | } 108 | 109 | 110 | public final void removeCallbacks(Runnable r, Object token) { 111 | final WeakRunnable runnable = mRunnables.remove(r); 112 | if (runnable != null) { 113 | mExec.removeCallbacks(runnable, token); 114 | } 115 | } 116 | 117 | 118 | public final boolean sendMessage(Message msg) { 119 | return mExec.sendMessage(msg); 120 | } 121 | 122 | 123 | public final boolean sendEmptyMessage(int what) { 124 | return mExec.sendEmptyMessage(what); 125 | } 126 | 127 | 128 | public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { 129 | return mExec.sendEmptyMessageDelayed(what, delayMillis); 130 | } 131 | 132 | public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) { 133 | return mExec.sendEmptyMessageAtTime(what, uptimeMillis); 134 | } 135 | 136 | 137 | public final boolean sendMessageDelayed(Message msg, long delayMillis) { 138 | return mExec.sendMessageDelayed(msg, delayMillis); 139 | } 140 | 141 | 142 | public boolean sendMessageAtTime(Message msg, long uptimeMillis) { 143 | return mExec.sendMessageAtTime(msg, uptimeMillis); 144 | } 145 | 146 | 147 | public final boolean sendMessageAtFrontOfQueue(Message msg) { 148 | return mExec.sendMessageAtFrontOfQueue(msg); 149 | } 150 | 151 | 152 | public final void removeMessages(int what) { 153 | mExec.removeMessages(what); 154 | } 155 | 156 | 157 | public final void removeMessages(int what, Object object) { 158 | mExec.removeMessages(what, object); 159 | } 160 | 161 | 162 | public final void removeCallbacksAndMessages(Object token) { 163 | mExec.removeCallbacksAndMessages(token); 164 | } 165 | 166 | 167 | public final boolean hasMessages(int what) { 168 | return mExec.hasMessages(what); 169 | } 170 | 171 | /** 172 | *检查是否有任何挂起邮件的帖子“代码”和“什么”, 173 | * 其对象是消息队列中的“对象” 174 | */ 175 | public final boolean hasMessages(int what, Object object) { 176 | return mExec.hasMessages(what, object); 177 | } 178 | 179 | public final Looper getLooper() { 180 | return mExec.getLooper(); 181 | } 182 | 183 | private WeakRunnable wrapRunnable(Runnable r) { 184 | //noinspection ConstantConditions 185 | if (r == null) { 186 | throw new NullPointerException("Runnable can't be null"); 187 | } 188 | final ChainedRef hardRef = new ChainedRef(mLock, r); 189 | mRunnables.insertAfter(hardRef); 190 | return hardRef.wrapper; 191 | } 192 | 193 | private static class ExecHandler extends Handler { 194 | private final WeakReference mCallback; 195 | 196 | ExecHandler() { 197 | mCallback = null; 198 | } 199 | 200 | ExecHandler(WeakReference callback) { 201 | mCallback = callback; 202 | } 203 | 204 | ExecHandler(Looper looper) { 205 | super(looper); 206 | mCallback = null; 207 | } 208 | 209 | ExecHandler(Looper looper, WeakReference callback) { 210 | super(looper); 211 | mCallback = callback; 212 | } 213 | 214 | @Override 215 | public void handleMessage(Message msg) { 216 | if (mCallback == null) { 217 | return; 218 | } 219 | final Callback callback = mCallback.get(); 220 | if (callback == null) { // Already disposed 221 | return; 222 | } 223 | callback.handleMessage(msg); 224 | } 225 | } 226 | 227 | static class WeakRunnable implements Runnable { 228 | private final WeakReference mDelegate; 229 | private final WeakReference mReference; 230 | 231 | WeakRunnable(WeakReference delegate, WeakReference reference) { 232 | mDelegate = delegate; 233 | mReference = reference; 234 | } 235 | 236 | @Override 237 | public void run() { 238 | final Runnable delegate = mDelegate.get(); 239 | final ChainedRef reference = mReference.get(); 240 | if (reference != null) { 241 | reference.remove(); 242 | } 243 | if (delegate != null) { 244 | delegate.run(); 245 | } 246 | } 247 | } 248 | 249 | static class ChainedRef { 250 | ChainedRef next; 251 | ChainedRef prev; 252 | final Runnable runnable; 253 | final WeakRunnable wrapper; 254 | 255 | Lock lock; 256 | 257 | public ChainedRef(Lock lock, Runnable r) { 258 | this.runnable = r; 259 | this.lock = lock; 260 | this.wrapper = new WeakRunnable(new WeakReference<>(r), new WeakReference<>(this)); 261 | } 262 | 263 | public WeakRunnable remove() { 264 | lock.lock(); 265 | try { 266 | if (prev != null) { 267 | prev.next = next; 268 | } 269 | if (next != null) { 270 | next.prev = prev; 271 | } 272 | prev = null; 273 | next = null; 274 | } finally { 275 | lock.unlock(); 276 | } 277 | return wrapper; 278 | } 279 | 280 | public void insertAfter(ChainedRef candidate) { 281 | lock.lock(); 282 | try { 283 | if (this.next != null) { 284 | this.next.prev = candidate; 285 | } 286 | 287 | candidate.next = this.next; 288 | this.next = candidate; 289 | candidate.prev = this; 290 | } finally { 291 | lock.unlock(); 292 | } 293 | } 294 | 295 | public WeakRunnable remove(Runnable obj) { 296 | lock.lock(); 297 | try { 298 | ChainedRef curr = this.next; // Skipping head 299 | while (curr != null) { 300 | if (curr.runnable == obj) { // We do comparison exactly how Handler does inside 301 | return curr.remove(); 302 | } 303 | curr = curr.next; 304 | } 305 | } finally { 306 | lock.unlock(); 307 | } 308 | return null; 309 | } 310 | } 311 | } -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/video/OnVideoEncodeListener.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.video; 2 | 3 | import android.media.MediaCodec; 4 | 5 | import java.nio.ByteBuffer; 6 | 7 | public interface OnVideoEncodeListener { 8 | void onVideoEncode(ByteBuffer bb, MediaCodec.BufferInfo bi); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/widget/CustomDialog.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.widget; 2 | 3 | 4 | import android.app.Dialog; 5 | import android.content.Context; 6 | import android.text.TextUtils; 7 | import android.view.Display; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.View.OnClickListener; 11 | import android.view.WindowManager; 12 | import android.widget.Button; 13 | import android.widget.EditText; 14 | import android.widget.FrameLayout; 15 | import android.widget.ImageView; 16 | import android.widget.LinearLayout; 17 | import android.widget.LinearLayout.LayoutParams; 18 | import android.widget.TextView; 19 | 20 | import com.test.screenimage.R; 21 | import com.test.screenimage.utils.SupportMultipleScreensUtil; 22 | 23 | 24 | public class CustomDialog { 25 | private Context context; 26 | private Dialog dialog; 27 | private LinearLayout lLayout_bg; 28 | private TextView txt_title; 29 | private TextView txt_msg; 30 | private Button btn_neg; 31 | private Button btn_pos; 32 | private ImageView img_line; 33 | private Display display; 34 | private boolean showTitle = false; 35 | private boolean showMsg = false; 36 | private boolean showPosBtn = false; 37 | private boolean showNegBtn = false; 38 | private boolean showEtMsg = false; 39 | private EditText et_msg; 40 | 41 | public CustomDialog(Context context) { 42 | this.context = context; 43 | WindowManager windowManager = (WindowManager) context 44 | .getSystemService(Context.WINDOW_SERVICE); 45 | display = windowManager.getDefaultDisplay(); 46 | } 47 | 48 | @SuppressWarnings("deprecation") 49 | public CustomDialog builder() { 50 | // 获取Dialog布局 51 | View view = LayoutInflater.from(context).inflate(R.layout.view_alertdialog, null); 52 | SupportMultipleScreensUtil.scale(view); 53 | // 获取自定义Dialog布局中的控件 54 | lLayout_bg = (LinearLayout) view.findViewById(R.id.lLayout_bg); 55 | txt_title = (TextView) view.findViewById(R.id.txt_title); 56 | txt_title.setVisibility(View.GONE); 57 | txt_msg = (TextView) view.findViewById(R.id.txt_msg); 58 | txt_msg.setVisibility(View.GONE); 59 | btn_neg = (Button) view.findViewById(R.id.btn_neg); 60 | btn_neg.setVisibility(View.GONE); 61 | btn_pos = (Button) view.findViewById(R.id.btn_pos); 62 | btn_pos.setVisibility(View.GONE); 63 | img_line = (ImageView) view.findViewById(R.id.img_line); 64 | et_msg = (EditText) view.findViewById(R.id.et_msg); 65 | img_line.setVisibility(View.GONE); 66 | 67 | // 定义Dialog布局和参数 68 | dialog = new Dialog(context, R.style.AlertDialogStyle); 69 | dialog.setContentView(view); 70 | 71 | // 调整dialog背景大小 72 | lLayout_bg.setLayoutParams(new FrameLayout.LayoutParams((int) (display.getWidth() * 0.75), 73 | LayoutParams.WRAP_CONTENT)); 74 | 75 | return this; 76 | } 77 | 78 | public CustomDialog setTitle(String title) { 79 | showTitle = true; 80 | if ("".equals(title) || title == null) { 81 | showTitle = false; 82 | txt_title.setText("标题"); 83 | } else { 84 | txt_title.setText(title); 85 | } 86 | return this; 87 | } 88 | 89 | // 重载函数 增加对ResId资源的支持 90 | public CustomDialog setTitle(int titleId) { 91 | String title = context.getResources().getString(titleId); 92 | showTitle = true; 93 | if ("".equals(title)) { 94 | txt_title.setText("标题"); 95 | } else { 96 | txt_title.setText(title); 97 | } 98 | return this; 99 | } 100 | 101 | public CustomDialog setMessage(CharSequence msg) { 102 | showMsg = true; 103 | if ("".equals(msg)) { 104 | txt_msg.setText("内容"); 105 | } else { 106 | txt_msg.setText(msg); 107 | } 108 | return this; 109 | } 110 | 111 | // 重载函数 增加对ResId资源的支持 112 | public CustomDialog setMessage(int msgId) { 113 | String msg = context.getResources().getString(msgId); 114 | showMsg = true; 115 | if ("".equals(msg)) { 116 | txt_msg.setText("内容"); 117 | } else { 118 | txt_msg.setText(msg); 119 | } 120 | return this; 121 | } 122 | 123 | public CustomDialog setCancelable(boolean cancel) { 124 | dialog.setCancelable(cancel); 125 | return this; 126 | } 127 | 128 | public CustomDialog setPositiveButton(String text, final OnClickListener listener) { 129 | showPosBtn = true; 130 | if ("".equals(text)) { 131 | btn_pos.setText("确定"); 132 | } else { 133 | btn_pos.setText(text); 134 | } 135 | btn_pos.setOnClickListener(new OnClickListener() { 136 | @Override 137 | public void onClick(View v) { 138 | listener.onClick(v); 139 | dialog.dismiss(); 140 | } 141 | }); 142 | return this; 143 | } 144 | 145 | // 重载函数 增加对ResId资源的支持 146 | public CustomDialog setPositiveButton(int textId, final OnClickListener listener) { 147 | String text = context.getResources().getString(textId); 148 | showPosBtn = true; 149 | if ("".equals(text)) { 150 | btn_pos.setText("确定"); 151 | } else { 152 | btn_pos.setText(text); 153 | } 154 | btn_pos.setOnClickListener(new OnClickListener() { 155 | @Override 156 | public void onClick(View v) { 157 | listener.onClick(v); 158 | dialog.dismiss(); 159 | } 160 | }); 161 | return this; 162 | } 163 | 164 | public CustomDialog setNegativeButton(String text, final OnClickListener listener) { 165 | showNegBtn = true; 166 | if ("".equals(text)) { 167 | btn_neg.setText("取消"); 168 | } else { 169 | btn_neg.setText(text); 170 | } 171 | btn_neg.setOnClickListener(new OnClickListener() { 172 | @Override 173 | public void onClick(View v) { 174 | listener.onClick(v); 175 | dialog.dismiss(); 176 | } 177 | }); 178 | return this; 179 | } 180 | 181 | // 重载函数 增加对ResId资源的支持 182 | public CustomDialog setNegativeButton(int textId, final OnClickListener listener) { 183 | String text = context.getResources().getString(textId); 184 | showNegBtn = true; 185 | if ("".equals(text)) { 186 | btn_neg.setText("取消"); 187 | } else { 188 | btn_neg.setText(text); 189 | } 190 | btn_neg.setOnClickListener(new OnClickListener() { 191 | @Override 192 | public void onClick(View v) { 193 | listener.onClick(v); 194 | dialog.dismiss(); 195 | } 196 | }); 197 | return this; 198 | } 199 | 200 | public CustomDialog setEtMsg(String text) { 201 | showEtMsg = true; 202 | if (!TextUtils.isEmpty(text)) { 203 | et_msg.setText(text); 204 | et_msg.setSelection(text.length()); 205 | } 206 | return this; 207 | } 208 | 209 | public String getEtMsg() { 210 | return et_msg.getText().toString(); 211 | } 212 | 213 | private void setLayout() { 214 | if (!showTitle) { 215 | // txt_title.setText("提示"); 216 | txt_title.setVisibility(View.GONE); 217 | } 218 | 219 | if (showTitle) { 220 | txt_title.setVisibility(View.VISIBLE); 221 | } 222 | 223 | if (showMsg) { 224 | txt_msg.setVisibility(View.VISIBLE); 225 | } 226 | 227 | if (showEtMsg) { 228 | et_msg.setVisibility(View.VISIBLE); 229 | } 230 | if (!showPosBtn && !showNegBtn) { 231 | btn_pos.setText("确定"); 232 | btn_pos.setVisibility(View.VISIBLE); 233 | btn_pos.setOnClickListener(new OnClickListener() { 234 | @Override 235 | public void onClick(View v) { 236 | dialog.dismiss(); 237 | } 238 | }); 239 | } 240 | 241 | if (showPosBtn && showNegBtn) { 242 | btn_pos.setVisibility(View.VISIBLE); 243 | btn_neg.setVisibility(View.VISIBLE); 244 | img_line.setVisibility(View.VISIBLE); 245 | } 246 | 247 | if (showPosBtn && !showNegBtn) { 248 | btn_pos.setVisibility(View.VISIBLE); 249 | 250 | } 251 | 252 | if (!showPosBtn && showNegBtn) { 253 | btn_neg.setVisibility(View.VISIBLE); 254 | 255 | } 256 | } 257 | 258 | public void setPosBackground(int color) { 259 | btn_pos.setBackgroundColor(color); 260 | } 261 | 262 | public EditText getEt_msg() { 263 | return et_msg; 264 | } 265 | 266 | public void show() { 267 | setLayout(); 268 | dialog.show(); 269 | } 270 | 271 | public void dismiss() { 272 | if (dialog == null) return; 273 | dialog.dismiss(); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /app/src/main/java/com/test/screenimage/widget/LoadingDialog.java: -------------------------------------------------------------------------------- 1 | package com.test.screenimage.widget; 2 | 3 | 4 | import android.app.Dialog; 5 | import android.content.Context; 6 | import android.view.Gravity; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.WindowManager.LayoutParams; 10 | 11 | import com.test.screenimage.R; 12 | import com.test.screenimage.utils.SupportMultipleScreensUtil; 13 | 14 | /** 15 | * 加载对话框控件 16 | * 17 | * @author wt 18 | * @version V1.0 创建时间:2017-3-7 19 | */ 20 | public class LoadingDialog extends Dialog { 21 | private LayoutParams lp; 22 | private LayoutInflater inflater; 23 | private Context mContext; 24 | 25 | public LoadingDialog(Context context, int theme) { 26 | super(context, theme); 27 | this.mContext = context; 28 | } 29 | 30 | public LoadingDialog(Context context) { 31 | super(context, R.style.MyLoadDialog); 32 | this.mContext = context; 33 | inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 34 | View layout = inflater.inflate(R.layout.loadingdialog, null); 35 | SupportMultipleScreensUtil.scale(layout); 36 | setContentView(layout); 37 | // 设置window属性 38 | lp = getWindow().getAttributes(); 39 | lp.gravity = Gravity.CENTER; 40 | lp.dimAmount = 0; // 去背景遮盖 41 | lp.alpha = 1.0f; 42 | getWindow().setAttributes(lp); 43 | setCanceledOnTouchOutside(true); 44 | setCancelable(true); 45 | } 46 | 47 | @Override 48 | public void onBackPressed() { 49 | super.onBackPressed(); 50 | this.dismiss(); 51 | } 52 | 53 | @Override 54 | public void dismiss() { 55 | super.dismiss(); 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtt123/ScreenImage/0fef35e3491004431639aadc5176a3d2341bf309/app/src/main/res/drawable-nodpi/ic_client.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_lamp_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtt123/ScreenImage/0fef35e3491004431639aadc5176a3d2341bf309/app/src/main/res/drawable-nodpi/ic_lamp_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_lamp_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtt123/ScreenImage/0fef35e3491004431639aadc5176a3d2341bf309/app/src/main/res/drawable-nodpi/ic_lamp_open.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_lamp_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtt123/ScreenImage/0fef35e3491004431639aadc5176a3d2341bf309/app/src/main/res/drawable-nodpi/ic_lamp_press.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtt123/ScreenImage/0fef35e3491004431639aadc5176a3d2341bf309/app/src/main/res/drawable-nodpi/ic_loading.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtt123/ScreenImage/0fef35e3491004431639aadc5176a3d2341bf309/app/src/main/res/drawable-nodpi/ic_network.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtt123/ScreenImage/0fef35e3491004431639aadc5176a3d2341bf309/app/src/main/res/drawable-nodpi/ic_server.png -------------------------------------------------------------------------------- /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/bg_recommend.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dialog_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/f_new_image_progress_popbox.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /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/drawable/scan_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtt123/ScreenImage/0fef35e3491004431639aadc5176a3d2341bf309/app/src/main/res/drawable/scan_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/select_lamp_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 19 | 20 | 26 | 27 | 33 | 34 | 41 | 42 | 43 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_scan_qr_code.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_screen_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 |