├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── question.md
└── stale.yml
├── .gitignore
├── .idea
└── codeStyles
│ └── Project.xml
├── License.txt
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── release
│ ├── HBRecorderDemo.apk
│ └── output-metadata.json
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── hbisoft
│ │ └── hbrecorderexample
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── hbisoft
│ │ │ └── hbrecorderexample
│ │ │ ├── MainActivity.java
│ │ │ └── SettingsActivity.java
│ └── res
│ │ ├── drawable
│ │ ├── icon.png
│ │ └── ripple_effect.xml
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── menu
│ │ └── menu_main.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ │ ├── values
│ │ ├── arrays.xml
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ ├── backup_descriptor.xml
│ │ └── pref_main.xml
│ └── test
│ └── java
│ └── com
│ └── hbisoft
│ └── hbrecorderexample
│ └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── hbrecorder
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── hbisoft
│ │ └── hbrecorder
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── hbisoft
│ │ │ └── hbrecorder
│ │ │ ├── Constants.java
│ │ │ ├── Countdown.java
│ │ │ ├── FileObserver.java
│ │ │ ├── HBRecorder.java
│ │ │ ├── HBRecorderCodecInfo.java
│ │ │ ├── HBRecorderListener.java
│ │ │ ├── MyListener.java
│ │ │ ├── NotificationReceiver.java
│ │ │ └── ScreenRecordService.java
│ └── res
│ │ ├── drawable
│ │ └── icon.png
│ │ └── values
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── hbisoft
│ └── hbrecorder
│ └── ExampleUnitTest.java
├── jitpack.yml
└── settings.gradle
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Issue template for a bug reports
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | READ THIS BEFORE OPENING AN ISSUE:
11 | -----------------------
12 | - Fill in all the information below.
13 | - Have a look at previously asked questions to see if your issue has been resolved.
14 | - Properly format your issue.
15 | - All issues should be asked in English.
16 | - Delete all the above before posting your issue
17 |
18 | **Describe the bug**
19 | A clear and concise description of what the bug is.
20 |
21 | **Log**
22 | Please provide a well formatted bug report (by adding 4 spaces before the log)
23 |
24 | **Can it be reproduced in demo app**
25 | Yes or no
26 |
27 | **HBRecorder version**
28 | for example 1.0.1
29 |
30 | **Device information**
31 | - Make/model
32 | - SDK version
33 |
34 | **Screenshots**
35 | If applicable, add screenshots to help explain your problem.
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask a question related to the project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | Before filing a bug:
11 | -----------------------
12 | - Have a look at previously asked questions to see if your question has been asked before.
13 | - Properly format your question.
14 | - All questions should be asked in English.
15 | - Questions that are not related to the project will be deleted
16 | - Delete all the above before posting
17 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Configuration for probot-stale - https://github.com/probot/stale
2 |
3 | # Number of days of inactivity before an Issue or Pull Request becomes stale
4 | daysUntilStale: 7
5 |
6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
8 | daysUntilClose: 3
9 |
10 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
11 | exemptLabels:
12 | - is:bug
13 | - is:enhancement
14 | - is:discussion
15 |
16 | # Label to use when marking as stale
17 | staleLabel: status:stale
18 |
19 | # Comment to post when marking as stale. Set to `false` to disable
20 | markComment: >
21 | This issue has been automatically marked as stale because it has not had
22 | activity in the last 7 days. It will be closed if no further activity
23 | occurs within the next 3 days. Thank you for your contributions.
24 | # Limit to only `issues` or `pulls`
25 | only: issues
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | /.idea/*.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | xmlns:android
14 |
15 | ^$
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | xmlns:.*
25 |
26 | ^$
27 |
28 |
29 | BY_NAME
30 |
31 |
32 |
33 |
34 |
35 |
36 | .*:id
37 |
38 | http://schemas.android.com/apk/res/android
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | .*:name
48 |
49 | http://schemas.android.com/apk/res/android
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | name
59 |
60 | ^$
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | style
70 |
71 | ^$
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | .*
81 |
82 | ^$
83 |
84 |
85 | BY_NAME
86 |
87 |
88 |
89 |
90 |
91 |
92 | .*
93 |
94 | http://schemas.android.com/apk/res/android
95 |
96 |
97 | ANDROID_ATTRIBUTE_ORDER
98 |
99 |
100 |
101 |
102 |
103 |
104 | .*
105 |
106 | .*
107 |
108 |
109 | BY_NAME
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/License.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2020] [HBiSoft]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **
Creating and maintaining a library like this requires a significant amount of time and effort.
**
2 |
3 | **If you’d like to show your appreciation, you can do so below:
**
4 |
5 | 
6 |
7 | ---
8 |
9 |
10 | # HBRecorder
11 | [](https://jitpack.io/#HBiSoft/HBRecorder)
12 | []( https://android-arsenal.com/details/1/7897 )
13 |
14 | Lightweight screen and audio recording Android library Requires API level 21>
15 |
16 | 
17 |
18 |
19 |
20 | Demo:
21 |
22 | Download the demo app here
23 |
24 |
25 |
26 |
27 |
28 |
29 | **Adding the library to your project:**
30 | ---
31 | Add the following in your root build.gradle at the end of repositories:
32 |
33 | ```java
34 | allprojects {
35 | repositories {
36 | ...
37 | maven { url 'https://jitpack.io' }
38 | }
39 | }
40 | ```
41 |
42 | Implement library in your app level build.gradle:
43 |
44 | ```java
45 | dependencies {
46 | implementation 'com.github.HBiSoft:HBRecorder:3.0.9'
47 | }
48 | ```
49 |
50 |
51 | **Implementing the library:**
52 | ---
53 | 1. In your `Activity`, first implement `HBRecorder`, as shown below:
54 |
55 | ```java
56 | public class MainActivity extends AppCompatActivity implements HBRecorderListener {
57 | ```
58 |
59 | 2. `Alt+Enter` to implement the following methods:
60 |
61 | ```java
62 | @Override
63 | public void HBRecorderOnStart() {
64 | //When the recording starts
65 | }
66 |
67 | @Override
68 | public void HBRecorderOnComplete() {
69 | //After file was created
70 | }
71 | @Override
72 | public void HBRecorderOnError(int errorCode) {
73 | //When an error occurs
74 | }
75 |
76 | @Override
77 | public void HBRecorderOnPause() {
78 | //When recording was paused
79 | }
80 |
81 | @Override
82 | public void HBRecorderOnResume() {
83 | //When recording was resumed
84 | }
85 | ```
86 |
87 | 3. Init `HBRecorder` as shown below:
88 | ```java
89 | public class MainActivity extends AppCompatActivity implements HBRecorderListener {
90 | HBRecorder hbRecorder;
91 |
92 | @Override
93 | protected void onCreate(Bundle savedInstanceState) {
94 | super.onCreate(savedInstanceState);
95 | setContentView(R.layout.activity_main);
96 |
97 | //Init HBRecorder
98 | hbRecorder = new HBRecorder(this, this);
99 |
100 | }
101 | ```
102 |
103 | 4. Add the following permissions in your manifest:
104 | ```java
105 |
106 |
107 |
108 |
109 |
110 |
111 | ```
112 |
113 | That's it, `HBRecorder` is now ready to be used.
114 |
115 | ---
116 |
117 | When you want to start capturing your screen, it is important you do it as shown below:
118 | ---
119 | ```java
120 | private void startRecordingScreen() {
121 | MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
122 | Intent permissionIntent = mediaProjectionManager != null ? mediaProjectionManager.createScreenCaptureIntent() : null;
123 | startActivityForResult(permissionIntent, SCREEN_RECORD_REQUEST_CODE);
124 | }
125 |
126 | @Override
127 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
128 | super.onActivityResult(requestCode, resultCode, data);
129 | if (requestCode == SCREEN_RECORD_REQUEST_CODE) {
130 | if (resultCode == RESULT_OK) {
131 | //Start screen recording
132 | hbRecorder.startScreenRecording(data, resultCode);
133 |
134 | }
135 | }
136 | }
137 | ```
138 |
139 | All available methods:
140 | ---
141 | ```java
142 | // Set the output path as a String
143 | // Only use this on devices running Android 9 and lower or you have to add android:requestLegacyExternalStorage="true" in your manifest
144 | // Defaults to - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
145 | hbrecorder.setOutputPath(String);
146 | // Set output Uri
147 | // Only use this on devices running Android 10>
148 | // When setting a Uri ensure you pass the same name to HBRecorder as what you set in ContentValues (DISPLAY_NAME and TITLE)
149 | hbRecorder.setOutputUri(Uri);
150 | // Set file name as String
151 | // Defaults to - quality + time stamp. For example HD-2019-08-14-10-09-58.mp4
152 | hbrecorder.setFileName(String);
153 | // Set audio bitrate as int
154 | // Defaults to - 128000
155 | hbrecorder.setAudioBitrate(int);
156 | // Set audio sample rate as int
157 | // Defaults to - 44100
158 | hbrecorder.setAudioSamplingRate(int);
159 | // Enable/Disable audio
160 | // Defaults to true
161 | hbrecorder.isAudioEnabled(boolean);
162 | // Enable/Disable HD Video
163 | // Defaults to true
164 | hbrecorder.recordHDVideo(boolean);
165 | // Get file path as String
166 | hbrecorder.getFilePath();
167 | // Get file name as String
168 | hbrecorder.getFileName();
169 | // Start recording screen by passing it as Intent inside onActivityResult
170 | hbrecorder.startScreenRecording(Intent);
171 | // Pause screen recording (only available for devices running 24>)
172 | hbrecorder.pauseScreenRecording();
173 | // Resume screen recording
174 | hbreccorder.resumeScreenRecording();
175 | // Stop screen recording
176 | hbrecorder.stopScreenRecording();
177 | // Check if recording is in progress
178 | hbrecorder.isBusyRecording();
179 | // Set notification icon by passing, for example R.drawable.myicon
180 | // Defaults to R.drawable.icon
181 | hbrecorder.setNotificationSmallIcon(int);
182 | // Set notification icon using byte array
183 | hbrecorder.setNotificationSmallIcon(byte[]);
184 | // Set notification icon using vector drawable
185 | hbRecorder.setNotificationSmallIconVector(vector);
186 | // Set notification title
187 | // Defaults to "Recording your screen"
188 | hbrecorder.setNotificationTitle(String);
189 | // Set notification description
190 | // Defaults to "Drag down to stop the recording"
191 | hbrecorder.setNotificationDescription(String);
192 | // Set notification stop button text
193 | // Defaults to "STOP RECORDING"
194 | hbrecorder.setNotificationButtonText(String);
195 | // Set output orientation (in degrees)
196 | hbrecorder.setOrientationHint(int);
197 | // Set max output file size
198 | hbrecorder.setMaxFileSize(long);
199 | // Set max time (in seconds)
200 | hbRecorder.setMaxDuration(int);
201 | ```
202 |
203 | Custom setting:
204 | ---
205 | When you want to enable custom settings you must first call:
206 | ```java
207 | hbRecorder.enableCustomSettings();
208 | ```
209 | Then you can set the following:
210 | ```java
211 | //MUST BE ONE OF THE FOLLOWING - https://developer.android.com/reference/android/media/MediaRecorder.AudioSource.html
212 | hbRecorder.setAudioSource(String);
213 | //MUST BE ONE OF THE FOLLOWING - https://developer.android.com/reference/android/media/MediaRecorder.VideoEncoder.html
214 | hbRecorder.setVideoEncoder(String);
215 | //If nothing is provided, it will select the highest value supported by your device
216 | hbRecorder.setScreenDimensions(HeightInPx, WidthInPx);
217 | //Frame rate is device dependent
218 | //You can use Camcoderprofile to determine the frame rate
219 | hbRecorder.setVideoFrameRate(int);
220 | //The bitrate is also dependent on the device and the frame rate that is set
221 | hbRecorder.setVideoBitrate(int);
222 | //MUST BE ONE OF THE FOLLOWING - https://developer.android.com/reference/android/media/MediaRecorder.OutputFormat.html
223 | hbRecorder.setOutputFormat(String);
224 | ```
225 |
226 | ---
227 | It is important to note that limitations are device dependent. It is best to set the video encoder to "DEFAULT" and let `MediaRecorder` pick the best encoder.
228 |
229 | In the demo app you will have the option to test different video encoders, bitrate, frame rate and output formats. If your device does not support any of the parameters you have selected `HBRecorderOnError` will be called.
230 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdk 34
5 | defaultConfig {
6 | applicationId "com.hbisoft.hbrecorderexample"
7 | minSdk 17
8 | targetSdk 34
9 | versionCode 1
10 | versionName "1.0"
11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
12 | }
13 | buildTypes {
14 | release {
15 | minifyEnabled false
16 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
17 | }
18 | }
19 |
20 | compileOptions {
21 | sourceCompatibility JavaVersion.VERSION_1_8
22 | targetCompatibility JavaVersion.VERSION_1_8
23 | }
24 | namespace 'com.hbisoft.hbrecorderexample'
25 |
26 | }
27 |
28 | dependencies {
29 | implementation fileTree(dir: 'libs', include: ['*.jar'])
30 | implementation 'androidx.appcompat:appcompat:1.6.1'
31 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
32 | implementation 'androidx.legacy:legacy-support-core-utils:1.0.0'
33 | implementation 'androidx.preference:preference:1.2.0'
34 | testImplementation 'junit:junit:4.13.2'
35 | androidTestImplementation 'androidx.test:runner:1.5.2'
36 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
37 | implementation project(path: ':hbrecorder')
38 | }
39 |
--------------------------------------------------------------------------------
/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/release/HBRecorderDemo.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HBiSoft/HBRecorder/b8c941de636b77376b818bebc61fd87ac4c03975/app/release/HBRecorderDemo.apk
--------------------------------------------------------------------------------
/app/release/output-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "artifactType": {
4 | "type": "APK",
5 | "kind": "Directory"
6 | },
7 | "applicationId": "com.hbisoft.hbrecorderexample",
8 | "variantName": "release",
9 | "elements": [
10 | {
11 | "type": "SINGLE",
12 | "filters": [],
13 | "attributes": [],
14 | "versionCode": 1,
15 | "versionName": "1.0",
16 | "outputFile": "app-release.apk"
17 | }
18 | ],
19 | "elementType": "File"
20 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/hbisoft/hbrecorderexample/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorderexample;
2 |
3 | import android.content.Context;
4 | import androidx.test.InstrumentationRegistry;
5 | import androidx.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.hbisoft.hbrecorderexample", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
24 |
25 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hbisoft/hbrecorderexample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorderexample;
2 |
3 | import android.Manifest;
4 | import android.content.ContentResolver;
5 | import android.content.ContentValues;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.content.SharedPreferences;
9 | import android.content.pm.PackageManager;
10 | import android.graphics.Bitmap;
11 | import android.graphics.BitmapFactory;
12 | import android.media.MediaScannerConnection;
13 | import android.media.projection.MediaProjectionManager;
14 | import android.net.Uri;
15 | import android.os.Build;
16 | import android.os.Bundle;
17 | import android.os.Environment;
18 | import android.preference.PreferenceManager;
19 | import android.provider.MediaStore;
20 | import android.util.Log;
21 | import android.view.Menu;
22 | import android.view.MenuItem;
23 | import android.view.View;
24 | import android.widget.Button;
25 | import android.widget.CheckBox;
26 | import android.widget.CompoundButton;
27 | import android.widget.EditText;
28 | import android.widget.RadioGroup;
29 | import androidx.appcompat.widget.SwitchCompat;
30 | import android.widget.Toast;
31 |
32 | import androidx.annotation.DrawableRes;
33 | import androidx.annotation.NonNull;
34 | import androidx.annotation.RequiresApi;
35 | import androidx.appcompat.app.AppCompatActivity;
36 | import androidx.core.app.ActivityCompat;
37 | import androidx.core.content.ContextCompat;
38 |
39 | import com.hbisoft.hbrecorder.HBRecorder;
40 | import com.hbisoft.hbrecorder.HBRecorderCodecInfo;
41 | import com.hbisoft.hbrecorder.HBRecorderListener;
42 |
43 | import java.io.ByteArrayOutputStream;
44 | import java.io.File;
45 | import java.sql.Date;
46 | import java.text.SimpleDateFormat;
47 | import java.util.ArrayList;
48 | import java.util.HashMap;
49 | import java.util.Locale;
50 | import java.util.Map;
51 |
52 | import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
53 | import static com.hbisoft.hbrecorder.Constants.MAX_FILE_SIZE_REACHED_ERROR;
54 | import static com.hbisoft.hbrecorder.Constants.SETTINGS_ERROR;
55 |
56 |
57 | /**
58 | * Created by HBiSoft on 13 Aug 2019
59 | * Copyright (c) 2019 . All rights reserved.
60 | */
61 |
62 | /*
63 | * Implementation Steps
64 | *
65 | * 1. Implement HBRecorderListener by calling implements HBRecorderListener
66 | * After this you have to implement the methods by pressing (Alt + Enter)
67 | *
68 | * 2. Declare HBRecorder
69 | *
70 | * 3. Init implements HBRecorderListener by calling hbRecorder = new HBRecorder(this, this);
71 | *
72 | * 4. Set adjust provided settings
73 | *
74 | * 5. Start recording by first calling:
75 | * MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
76 | Intent permissionIntent = mediaProjectionManager != null ? mediaProjectionManager.createScreenCaptureIntent() : null;
77 | startActivityForResult(permissionIntent, SCREEN_RECORD_REQUEST_CODE);
78 |
79 | * 6. Then in onActivityResult call hbRecorder.onActivityResult(resultCode, data, this);
80 | *
81 | * 7. Then you can start recording by calling hbRecorder.startScreenRecording(data);
82 | *
83 | * */
84 |
85 | @SuppressWarnings({"SameParameterValue"})
86 | public class MainActivity extends AppCompatActivity implements HBRecorderListener {
87 | //Permissions
88 | private static final int SCREEN_RECORD_REQUEST_CODE = 777;
89 | private static final int PERMISSION_REQ_ID_RECORD_AUDIO = 22;
90 | private static final int PERMISSION_REQ_POST_NOTIFICATIONS = 33;
91 | private static final int PERMISSION_REQ_ID_WRITE_EXTERNAL_STORAGE = PERMISSION_REQ_ID_RECORD_AUDIO + 1;
92 | private static final int PERMISSION_REQ_ID_FOREGROUND_SERVICE_MEDIA_PROJECTION = PERMISSION_REQ_ID_WRITE_EXTERNAL_STORAGE + 1;
93 | private boolean hasPermissions = false;
94 | private boolean hasAudioPermissions = false;
95 |
96 | //Declare HBRecorder
97 | private HBRecorder hbRecorder;
98 |
99 | //Start/Stop Button
100 | private Button startbtn;
101 |
102 | //HD/SD quality
103 | private RadioGroup radioGroup;
104 |
105 | //Should record/show audio/notification
106 | private CheckBox recordAudioCheckBox;
107 |
108 | //Reference to checkboxes and radio buttons
109 | boolean wasHDSelected = true;
110 | boolean isAudioEnabled = true;
111 |
112 | //Should custom settings be used
113 | SwitchCompat custom_settings_switch;
114 |
115 | // Max file size in K
116 | private EditText maxFileSizeInK;
117 |
118 |
119 | @Override
120 | protected void onCreate(Bundle savedInstanceState) {
121 | super.onCreate(savedInstanceState);
122 | setContentView(R.layout.activity_main);
123 |
124 | initViews();
125 | setOnClickListeners();
126 | setRadioGroupCheckListener();
127 | setRecordAudioCheckBoxListener();
128 |
129 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
130 | //Init HBRecorder
131 | hbRecorder = new HBRecorder(this, this);
132 |
133 | //When the user returns to the application, some UI changes might be necessary,
134 | //check if recording is in progress and make changes accordingly
135 | if (hbRecorder.isBusyRecording()) {
136 | startbtn.setText(R.string.stop_recording);
137 | }
138 | }
139 |
140 | // Examples of how to use the HBRecorderCodecInfo class to get codec info
141 | HBRecorderCodecInfo hbRecorderCodecInfo = new HBRecorderCodecInfo();
142 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
143 | int mWidth = hbRecorder.getDefaultWidth();
144 | int mHeight = hbRecorder.getDefaultHeight();
145 | String mMimeType = "video/avc";
146 | int mFPS = 30;
147 | if (hbRecorderCodecInfo.isMimeTypeSupported(mMimeType)) {
148 | String defaultVideoEncoder = hbRecorderCodecInfo.getDefaultVideoEncoderName(mMimeType);
149 | boolean isSizeAndFramerateSupported = hbRecorderCodecInfo.isSizeAndFramerateSupported(mWidth, mHeight, mFPS, mMimeType, ORIENTATION_PORTRAIT);
150 | Log.e("EXAMPLE", "THIS IS AN EXAMPLE OF HOW TO USE THE (HBRecorderCodecInfo) TO GET CODEC INFO:");
151 | Log.e("HBRecorderCodecInfo", "defaultVideoEncoder for (" + mMimeType + ") -> " + defaultVideoEncoder);
152 | Log.e("HBRecorderCodecInfo", "MaxSupportedFrameRate -> " + hbRecorderCodecInfo.getMaxSupportedFrameRate(mWidth, mHeight, mMimeType));
153 | Log.e("HBRecorderCodecInfo", "MaxSupportedBitrate -> " + hbRecorderCodecInfo.getMaxSupportedBitrate(mMimeType));
154 | Log.e("HBRecorderCodecInfo", "isSizeAndFramerateSupported @ Width = "+mWidth+" Height = "+mHeight+" FPS = "+mFPS+" -> " + isSizeAndFramerateSupported);
155 | Log.e("HBRecorderCodecInfo", "isSizeSupported @ Width = "+mWidth+" Height = "+mHeight+" -> " + hbRecorderCodecInfo.isSizeSupported(mWidth, mHeight, mMimeType));
156 | Log.e("HBRecorderCodecInfo", "Default Video Format = " + hbRecorderCodecInfo.getDefaultVideoFormat());
157 |
158 | HashMap supportedVideoMimeTypes = hbRecorderCodecInfo.getSupportedVideoMimeTypes();
159 | for (Map.Entry entry : supportedVideoMimeTypes.entrySet()) {
160 | Log.e("HBRecorderCodecInfo", "Supported VIDEO encoders and mime types : " + entry.getKey() + " -> " + entry.getValue());
161 | }
162 |
163 | HashMap supportedAudioMimeTypes = hbRecorderCodecInfo.getSupportedAudioMimeTypes();
164 | for (Map.Entry entry : supportedAudioMimeTypes.entrySet()) {
165 | Log.e("HBRecorderCodecInfo", "Supported AUDIO encoders and mime types : " + entry.getKey() + " -> " + entry.getValue());
166 | }
167 |
168 | ArrayList supportedVideoFormats = hbRecorderCodecInfo.getSupportedVideoFormats();
169 | for (int j = 0; j < supportedVideoFormats.size(); j++) {
170 | Log.e("HBRecorderCodecInfo", "Available Video Formats : " + supportedVideoFormats.get(j));
171 | }
172 | }else{
173 | Log.e("HBRecorderCodecInfo", "MimeType not supported");
174 | }
175 |
176 | }
177 |
178 | }
179 |
180 | //Create Folder
181 | //Only call this on Android 9 and lower (getExternalStoragePublicDirectory is deprecated)
182 | //This can still be used on Android 10> but you will have to add android:requestLegacyExternalStorage="true" in your Manifest
183 | private void createFolder() {
184 | File f1 = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), "HBRecorder");
185 | if (!f1.exists()) {
186 | if (f1.mkdirs()) {
187 | Log.i("Folder ", "created");
188 | }
189 | }
190 | }
191 |
192 | //Init Views
193 | private void initViews() {
194 | startbtn = findViewById(R.id.button_start);
195 | radioGroup = findViewById(R.id.radio_group);
196 | recordAudioCheckBox = findViewById(R.id.audio_check_box);
197 | custom_settings_switch = findViewById(R.id.custom_settings_switch);
198 | }
199 |
200 | private void saveAudioPreference(boolean isEnabled) {
201 | SharedPreferences preferences = getSharedPreferences("AppPreferences", MODE_PRIVATE);
202 | SharedPreferences.Editor editor = preferences.edit();
203 | editor.putBoolean("hasAudioPermissions", isEnabled);
204 | editor.apply();
205 | }
206 |
207 | private boolean hasAudioPreference() {
208 | SharedPreferences preferences = getSharedPreferences("AppPreferences", MODE_PRIVATE);
209 | return preferences.getBoolean("hasAudioPermissions", false); // Default to false if not set
210 | }
211 |
212 | //Start Button OnClickListener
213 | private void setOnClickListeners() {
214 | startbtn.setOnClickListener(v -> {
215 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
216 | // first check if permissions were granted
217 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // SDK 34
218 | if (isAudioEnabled) {
219 | if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS, PERMISSION_REQ_POST_NOTIFICATIONS)
220 | && checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)
221 | && checkSelfPermission(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION, PERMISSION_REQ_ID_FOREGROUND_SERVICE_MEDIA_PROJECTION)) {
222 | hasPermissions = true;
223 | saveAudioPreference(true);
224 | }
225 | }else{
226 | if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS, PERMISSION_REQ_POST_NOTIFICATIONS)
227 | && checkSelfPermission(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION, PERMISSION_REQ_ID_FOREGROUND_SERVICE_MEDIA_PROJECTION)) {
228 | hasPermissions = true;
229 | saveAudioPreference(false);
230 | }
231 | }
232 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // SDK 33
233 | if (isAudioEnabled) {
234 | if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS, PERMISSION_REQ_POST_NOTIFICATIONS)
235 | && checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)) {
236 | hasPermissions = true;
237 | saveAudioPreference(true);
238 | }
239 | }else{
240 | if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS, PERMISSION_REQ_POST_NOTIFICATIONS)) {
241 | hasPermissions = true;
242 | }
243 | }
244 |
245 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
246 | if (checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)) {
247 | hasPermissions = true;
248 | saveAudioPreference(true);
249 |
250 | }
251 | } else {
252 | if (checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)
253 | && checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, PERMISSION_REQ_ID_WRITE_EXTERNAL_STORAGE)) {
254 | hasPermissions = true;
255 | saveAudioPreference(true);
256 | }
257 | }
258 |
259 | if (hasPermissions) {
260 | // check if recording is in progress and stop it if it is
261 | if (hbRecorder.isBusyRecording()) {
262 | hbRecorder.stopScreenRecording();
263 | startbtn.setText(R.string.start_recording);
264 | } else {
265 | // else start recording
266 | if (!hasAudioPermissions && isAudioEnabled) {
267 | if (checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)){
268 | hasPermissions = true;
269 | saveAudioPreference(true);
270 | startRecordingScreen();
271 | }
272 |
273 | }else {
274 | startRecordingScreen();
275 | }
276 | }
277 | }
278 | } else {
279 | showLongToast("This library requires API 21>");
280 | }
281 | });
282 | }
283 |
284 | //Check if HD/SD Video should be recorded
285 | private void setRadioGroupCheckListener() {
286 | radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
287 | @Override
288 | public void onCheckedChanged(RadioGroup radioGroup, int checkedId) {
289 |
290 | if (checkedId == R.id.hd_button) {
291 | //Ser HBRecorder to HD
292 | wasHDSelected = true;
293 | } else if (checkedId == R.id.sd_button) {
294 | //Ser HBRecorder to SD
295 | wasHDSelected = false;
296 | }
297 | }
298 | });
299 | }
300 |
301 | //Check if audio should be recorded
302 | private void setRecordAudioCheckBoxListener() {
303 | recordAudioCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
304 | @Override
305 | public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
306 | //Enable/Disable audio
307 | isAudioEnabled = isChecked;
308 | }
309 | });
310 | }
311 |
312 | // Called when recording starts
313 | @Override
314 | public void HBRecorderOnStart() {
315 | Log.e("HBRecorder", "HBRecorderOnStart called");
316 | }
317 |
318 | //Listener for when the recording is saved successfully
319 | //This will be called after the file was created
320 | @Override
321 | public void HBRecorderOnComplete() {
322 | startbtn.setText(R.string.start_recording);
323 | showLongToast("Saved Successfully");
324 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
325 | //Update gallery depending on SDK Level
326 | if (hbRecorder.wasUriSet()) {
327 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ) {
328 | updateGalleryUri();
329 | } else {
330 | refreshGalleryFile();
331 | }
332 | }else{
333 | refreshGalleryFile();
334 | }
335 | }
336 |
337 | }
338 |
339 | // Called when error occurs
340 | @Override
341 | public void HBRecorderOnError(int errorCode, String reason) {
342 | // Error 38 happens when
343 | // - the selected video encoder is not supported
344 | // - the output format is not supported
345 | // - if another app is using the microphone
346 |
347 | //It is best to use device default
348 |
349 | if (errorCode == SETTINGS_ERROR) {
350 | showLongToast(getString(R.string.settings_not_supported_message));
351 | } else if ( errorCode == MAX_FILE_SIZE_REACHED_ERROR) {
352 | showLongToast(getString(R.string.max_file_size_reached_message));
353 | } else {
354 | showLongToast(getString(R.string.general_recording_error_message));
355 | Log.e("HBRecorderOnError", reason);
356 | }
357 |
358 | startbtn.setText(R.string.start_recording);
359 |
360 | }
361 |
362 | // Called when recording has been paused
363 | @Override
364 | public void HBRecorderOnPause() {
365 | // Called when recording was paused
366 | }
367 |
368 | // Calld when recording has resumed
369 | @Override
370 | public void HBRecorderOnResume() {
371 | // Called when recording was resumed
372 | }
373 |
374 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
375 | private void refreshGalleryFile() {
376 | MediaScannerConnection.scanFile(this,
377 | new String[]{hbRecorder.getFilePath()}, null,
378 | new MediaScannerConnection.OnScanCompletedListener() {
379 | public void onScanCompleted(String path, Uri uri) {
380 | Log.i("ExternalStorage", "Scanned " + path + ":");
381 | Log.i("ExternalStorage", "-> uri=" + uri);
382 | }
383 | });
384 | }
385 |
386 | @RequiresApi(api = Build.VERSION_CODES.Q)
387 | private void updateGalleryUri(){
388 | contentValues.clear();
389 | contentValues.put(MediaStore.Video.Media.IS_PENDING, 0);
390 | getContentResolver().update(mUri, contentValues, null, null);
391 | }
392 |
393 | //Start recording screen
394 | //It is important to call it like this
395 | //hbRecorder.startScreenRecording(data); should only be called in onActivityResult
396 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
397 | private void startRecordingScreen() {
398 | if (custom_settings_switch.isChecked()) {
399 | //WHEN SETTING CUSTOM SETTINGS YOU MUST SET THIS!!!
400 | hbRecorder.enableCustomSettings();
401 | customSettings();
402 | MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
403 | Intent permissionIntent = mediaProjectionManager != null ? mediaProjectionManager.createScreenCaptureIntent() : null;
404 | startActivityForResult(permissionIntent, SCREEN_RECORD_REQUEST_CODE);
405 | } else {
406 | quickSettings();
407 | MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
408 | Intent permissionIntent = mediaProjectionManager != null ? mediaProjectionManager.createScreenCaptureIntent() : null;
409 | startActivityForResult(permissionIntent, SCREEN_RECORD_REQUEST_CODE);
410 | }
411 | startbtn.setText(R.string.stop_recording);
412 | }
413 |
414 | String output_format;
415 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
416 | // Example of how to set custom settings
417 | private void customSettings() {
418 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
419 |
420 | //Is audio enabled
421 | boolean audio_enabled = prefs.getBoolean("key_record_audio", true);
422 | hbRecorder.isAudioEnabled(audio_enabled);
423 |
424 | //Audio Source
425 | String audio_source = prefs.getString("key_audio_source", null);
426 | if (audio_source != null) {
427 | switch (audio_source) {
428 | case "0":
429 | hbRecorder.setAudioSource("DEFAULT");
430 | break;
431 | case "1":
432 | hbRecorder.setAudioSource("CAMCODER");
433 | break;
434 | case "2":
435 | hbRecorder.setAudioSource("MIC");
436 | break;
437 | }
438 | }
439 |
440 | //Video Encoder
441 | String video_encoder = prefs.getString("key_video_encoder", null);
442 | if (video_encoder != null) {
443 | switch (video_encoder) {
444 | case "0":
445 | hbRecorder.setVideoEncoder("DEFAULT");
446 | break;
447 | case "1":
448 | hbRecorder.setVideoEncoder("H264");
449 | break;
450 | case "2":
451 | hbRecorder.setVideoEncoder("H263");
452 | break;
453 | case "3":
454 | hbRecorder.setVideoEncoder("HEVC");
455 | break;
456 | case "4":
457 | hbRecorder.setVideoEncoder("MPEG_4_SP");
458 | break;
459 | case "5":
460 | hbRecorder.setVideoEncoder("VP8");
461 | break;
462 | }
463 | }
464 |
465 | //NOTE - THIS MIGHT NOT BE SUPPORTED SIZES FOR YOUR DEVICE
466 | //Video Dimensions
467 | String video_resolution = prefs.getString("key_video_resolution", null);
468 | if (video_resolution != null) {
469 | switch (video_resolution) {
470 | case "0":
471 | hbRecorder.setScreenDimensions(426, 240);
472 | break;
473 | case "1":
474 | hbRecorder.setScreenDimensions(640, 360);
475 | break;
476 | case "2":
477 | hbRecorder.setScreenDimensions(854, 480);
478 | break;
479 | case "3":
480 | hbRecorder.setScreenDimensions(1280, 720);
481 | break;
482 | case "4":
483 | hbRecorder.setScreenDimensions(1920, 1080);
484 | break;
485 | }
486 | }
487 |
488 | //Video Frame Rate
489 | String video_frame_rate = prefs.getString("key_video_fps", null);
490 | if (video_frame_rate != null) {
491 | switch (video_frame_rate) {
492 | case "0":
493 | hbRecorder.setVideoFrameRate(60);
494 | break;
495 | case "1":
496 | hbRecorder.setVideoFrameRate(50);
497 | break;
498 | case "2":
499 | hbRecorder.setVideoFrameRate(48);
500 | break;
501 | case "3":
502 | hbRecorder.setVideoFrameRate(30);
503 | break;
504 | case "4":
505 | hbRecorder.setVideoFrameRate(25);
506 | break;
507 | case "5":
508 | hbRecorder.setVideoFrameRate(24);
509 | break;
510 | }
511 | }
512 |
513 | //Video Bitrate
514 | String video_bit_rate = prefs.getString("key_video_bitrate", null);
515 | if (video_bit_rate != null) {
516 | switch (video_bit_rate) {
517 | case "1":
518 | hbRecorder.setVideoBitrate(12000000);
519 | break;
520 | case "2":
521 | hbRecorder.setVideoBitrate(8000000);
522 | break;
523 | case "3":
524 | hbRecorder.setVideoBitrate(7500000);
525 | break;
526 | case "4":
527 | hbRecorder.setVideoBitrate(5000000);
528 | break;
529 | case "5":
530 | hbRecorder.setVideoBitrate(4000000);
531 | break;
532 | case "6":
533 | hbRecorder.setVideoBitrate(2500000);
534 | break;
535 | case "7":
536 | hbRecorder.setVideoBitrate(1500000);
537 | break;
538 | case "8":
539 | hbRecorder.setVideoBitrate(1000000);
540 | break;
541 | }
542 | }
543 |
544 | //Output Format
545 | output_format = prefs.getString("key_output_format", null);
546 | if (output_format != null) {
547 | switch (output_format) {
548 | case "0":
549 | hbRecorder.setOutputFormat("DEFAULT");
550 | break;
551 | case "1":
552 | hbRecorder.setOutputFormat("MPEG_4");
553 | break;
554 | case "2":
555 | hbRecorder.setOutputFormat("THREE_GPP");
556 | break;
557 | case "3":
558 | hbRecorder.setOutputFormat("WEBM");
559 | break;
560 | }
561 | }
562 |
563 | }
564 |
565 | //Get/Set the selected settings
566 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
567 | private void quickSettings() {
568 | hbRecorder.setAudioBitrate(128000);
569 | hbRecorder.setAudioSamplingRate(44100);
570 | hbRecorder.recordHDVideo(wasHDSelected);
571 | hbRecorder.isAudioEnabled(isAudioEnabled);
572 | //Customise Notification
573 | hbRecorder.setNotificationSmallIcon(R.drawable.icon);
574 | //hbRecorder.setNotificationSmallIconVector(R.drawable.ic_baseline_videocam_24);
575 | hbRecorder.setNotificationTitle(getString(R.string.stop_recording_notification_title));
576 | hbRecorder.setNotificationDescription(getString(R.string.stop_recording_notification_message));
577 | }
578 |
579 | // Example of how to set the max file size
580 |
581 | /*@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
582 | private void setRecorderMaxFileSize() {
583 | String s = maxFileSizeInK.getText().toString();
584 | long maxFileSizeInKilobytes;
585 | try {
586 | maxFileSizeInKilobytes = Long.parseLong(s);
587 | } catch (NumberFormatException e) {
588 | maxFileSizeInKilobytes = 0;
589 | }
590 | hbRecorder.setMaxFileSize(maxFileSizeInKilobytes * 1024); // Convert to bytes
591 |
592 | }*/
593 |
594 | @Override
595 | public boolean onCreateOptionsMenu(Menu menu) {
596 | getMenuInflater().inflate(R.menu.menu_main, menu);
597 | return true;
598 | }
599 |
600 | @Override
601 | public boolean onOptionsItemSelected(MenuItem item) {
602 | int id = item.getItemId();
603 |
604 | if (id == R.id.action_settings) {
605 | // launch settings activity
606 | startActivity(new Intent(MainActivity.this, SettingsActivity.class));
607 | return true;
608 | }
609 |
610 | return super.onOptionsItemSelected(item);
611 | }
612 |
613 | //Check if permissions was granted
614 | private boolean checkSelfPermission(String permission, int requestCode) {
615 | if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
616 | ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode);
617 | return false;
618 | }
619 | return true;
620 | }
621 |
622 | //Handle permissions
623 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
624 | @Override
625 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
626 | super.onRequestPermissionsResult(requestCode, permissions, grantResults);
627 | switch (requestCode) {
628 | case PERMISSION_REQ_POST_NOTIFICATIONS:
629 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
630 | if (isAudioEnabled) {
631 | checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO);
632 | }else {
633 | checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, PERMISSION_REQ_ID_WRITE_EXTERNAL_STORAGE);
634 | }
635 | } else {
636 | hasPermissions = false;
637 | showLongToast("No permission for " + Manifest.permission.POST_NOTIFICATIONS);
638 | }
639 | break;
640 | case PERMISSION_REQ_ID_RECORD_AUDIO:
641 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
642 | checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, PERMISSION_REQ_ID_WRITE_EXTERNAL_STORAGE);
643 | } else {
644 | hasPermissions = false;
645 | showLongToast("No permission for " + Manifest.permission.RECORD_AUDIO);
646 | }
647 | break;
648 | case PERMISSION_REQ_ID_WRITE_EXTERNAL_STORAGE:
649 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
650 | hasPermissions = true;
651 | startRecordingScreen();
652 | } else {
653 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
654 | checkSelfPermission(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION, PERMISSION_REQ_ID_FOREGROUND_SERVICE_MEDIA_PROJECTION);
655 | } else {
656 | hasPermissions = false;
657 | showLongToast("No permission for " + Manifest.permission.WRITE_EXTERNAL_STORAGE);
658 | }
659 | }
660 | break;
661 | case PERMISSION_REQ_ID_FOREGROUND_SERVICE_MEDIA_PROJECTION:
662 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
663 | hasPermissions = true;
664 | startRecordingScreen();
665 | } else {
666 | hasPermissions = false;
667 | showLongToast("No permission for " + Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION);
668 | }
669 | break;
670 | default:
671 | break;
672 | }
673 | }
674 |
675 |
676 | @Override
677 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
678 | super.onActivityResult(requestCode, resultCode, data);
679 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
680 | if (requestCode == SCREEN_RECORD_REQUEST_CODE) {
681 | if (resultCode == RESULT_OK) {
682 | //Set file path or Uri depending on SDK version
683 | setOutputPath();
684 | //Start screen recording
685 | hbRecorder.startScreenRecording(data, resultCode);
686 |
687 | }else{
688 | startbtn.setText(R.string.start_recording);
689 | }
690 | }
691 | }
692 | }
693 |
694 | //For Android 10> we will pass a Uri to HBRecorder
695 | //This is not necessary - You can still use getExternalStoragePublicDirectory
696 | //But then you will have to add android:requestLegacyExternalStorage="true" in your Manifest
697 | //IT IS IMPORTANT TO SET THE FILE NAME THE SAME AS THE NAME YOU USE FOR TITLE AND DISPLAY_NAME
698 | ContentResolver resolver;
699 | ContentValues contentValues;
700 | Uri mUri;
701 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
702 | private void setOutputPath() {
703 | String filename = generateFileName();
704 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
705 | resolver = getContentResolver();
706 | contentValues = new ContentValues();
707 | contentValues.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/" + "HBRecorder");
708 | contentValues.put(MediaStore.Video.Media.TITLE, filename);
709 | contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, filename);
710 | if (output_format != null) {
711 | contentValues.put(MediaStore.MediaColumns.MIME_TYPE, getMimeTypeForOutputFormat(output_format));
712 | }else {
713 | contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
714 | }
715 | mUri = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues);
716 | //FILE NAME SHOULD BE THE SAME
717 | hbRecorder.setFileName(filename);
718 | hbRecorder.setOutputUri(mUri);
719 | }else{
720 | createFolder();
721 | hbRecorder.setOutputPath(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) +"/HBRecorder");
722 | }
723 | }
724 |
725 | // Passing the MIME_TYPE to ContentValues() depending on what output format was selected
726 | // This is just to demonstrate for the demo app - more can be added
727 | private String getMimeTypeForOutputFormat(String outputFormat) {
728 | String mimetype = "video/mp4";
729 | switch (outputFormat) {
730 | // We do not know what the devices DEFAULT (0) is
731 | // For the sake of this demo app we will set it to mp4
732 | case "0":
733 | mimetype = "video/mp4";
734 | break;
735 | case "1":
736 | mimetype = "video/mp4";
737 | break;
738 | case "2":
739 | mimetype = "video/3gpp";
740 | break;
741 | case "3":
742 | mimetype = "video/webm";
743 | break;
744 | default:
745 | mimetype = "video/mp4";
746 | break;
747 | }
748 | return mimetype;
749 | }
750 |
751 | //Generate a timestamp to be used as a file name
752 | private String generateFileName() {
753 | SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault());
754 | Date curDate = new Date(System.currentTimeMillis());
755 | return formatter.format(curDate).replace(" ", "");
756 | }
757 |
758 | //Show Toast
759 | private void showLongToast(final String msg) {
760 | Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();
761 | }
762 |
763 | //drawable to byte[]
764 | private byte[] drawable2ByteArray(@DrawableRes int drawableId) {
765 | Bitmap icon = BitmapFactory.decodeResource(getResources(), drawableId);
766 | ByteArrayOutputStream stream = new ByteArrayOutputStream();
767 | icon.compress(Bitmap.CompressFormat.PNG, 100, stream);
768 | return stream.toByteArray();
769 | }
770 | }
771 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hbisoft/hbrecorderexample/SettingsActivity.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorderexample;
2 |
3 | import android.content.SharedPreferences;
4 | import android.os.Bundle;
5 | import android.view.MenuItem;
6 |
7 | import androidx.appcompat.app.AppCompatActivity;
8 | import androidx.preference.ListPreference;
9 | import androidx.preference.Preference;
10 | import androidx.preference.PreferenceFragmentCompat;
11 | import androidx.preference.PreferenceManager;
12 | import androidx.preference.SwitchPreference;
13 |
14 | public class SettingsActivity extends AppCompatActivity {
15 |
16 | @Override
17 | protected void onCreate(Bundle savedInstanceState) {
18 | super.onCreate(savedInstanceState);
19 | if (getSupportActionBar()!=null) {
20 | getSupportActionBar().setDisplayHomeAsUpEnabled(true);
21 | }
22 |
23 | // load settings fragment
24 | getSupportFragmentManager().beginTransaction().replace(android.R.id.content, new MainPreferenceFragment()).commit();
25 | }
26 |
27 | public static class MainPreferenceFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceChangeListener{
28 | ListPreference key_video_resolution, key_audio_source, key_video_encoder, key_video_fps, key_video_bitrate, key_output_format;
29 | SwitchPreference key_record_audio;
30 |
31 | @Override
32 | public void onCreate(final Bundle savedInstanceState) {
33 | super.onCreate(savedInstanceState);
34 | addPreferencesFromResource(R.xml.pref_main);
35 |
36 | key_record_audio = findPreference(getString(R.string.key_record_audio));
37 |
38 | key_audio_source = findPreference(getString(R.string.key_audio_source));
39 | if (key_audio_source != null) {
40 | key_audio_source.setOnPreferenceChangeListener(this);
41 | }
42 |
43 | key_video_encoder = findPreference(getString(R.string.key_video_encoder));
44 | if (key_video_encoder != null) {
45 | key_video_encoder.setOnPreferenceChangeListener(this);
46 | }
47 |
48 | key_video_resolution = findPreference(getString(R.string.key_video_resolution));
49 | if (key_video_resolution != null) {
50 | key_video_resolution.setOnPreferenceChangeListener(this);
51 | }
52 |
53 | key_video_fps = findPreference(getString(R.string.key_video_fps));
54 | if (key_video_fps != null) {
55 | key_video_fps.setOnPreferenceChangeListener(this);
56 | }
57 |
58 | key_video_bitrate = findPreference(getString(R.string.key_video_bitrate));
59 | if (key_video_bitrate != null) {
60 | key_video_bitrate.setOnPreferenceChangeListener(this);
61 | }
62 |
63 | key_output_format = findPreference(getString(R.string.key_output_format));
64 | if (key_output_format != null) {
65 | key_output_format.setOnPreferenceChangeListener(this);
66 | }
67 |
68 | setPreviousSelectedAsSummary();
69 |
70 | }
71 |
72 | @Override
73 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
74 |
75 | }
76 |
77 | @Override
78 | public boolean onPreferenceChange(Preference preference, Object newValue) {
79 | String preferenceKey = preference.getKey();
80 | ListPreference listPreference;
81 | switch (preferenceKey) {
82 | case "key_audio_source":
83 | listPreference = findPreference(getString(R.string.key_audio_source));
84 | if (listPreference != null) {
85 | listPreference.setSummary(listPreference.getEntries()[listPreference.findIndexOfValue(newValue.toString())]);
86 | }
87 | break;
88 | case "key_video_encoder":
89 | listPreference = findPreference(getString(R.string.key_video_encoder));
90 | if (listPreference != null) {
91 | listPreference.setSummary(listPreference.getEntries()[listPreference.findIndexOfValue(newValue.toString())]);
92 | listPreference.setValue(newValue.toString());
93 | }
94 | break;
95 | case "key_video_resolution":
96 | listPreference = findPreference(getString(R.string.key_video_resolution));
97 | if (listPreference != null) {
98 | listPreference.setSummary(listPreference.getEntries()[listPreference.findIndexOfValue(newValue.toString())]);
99 | listPreference.setValue(newValue.toString());
100 | }
101 | break;
102 | case "key_video_fps":
103 | listPreference = findPreference(getString(R.string.key_video_fps));
104 | if (listPreference != null) {
105 | listPreference.setSummary(listPreference.getEntries()[listPreference.findIndexOfValue(newValue.toString())]);
106 | listPreference.setValue(newValue.toString());
107 | }
108 |
109 | break;
110 | case "key_video_bitrate":
111 | listPreference = findPreference(getString(R.string.key_video_bitrate));
112 | if (listPreference != null) {
113 | listPreference.setSummary(listPreference.getEntries()[listPreference.findIndexOfValue(newValue.toString())]);
114 | listPreference.setValue(newValue.toString());
115 | }
116 | break;
117 | case "key_output_format":
118 | listPreference = findPreference(getString(R.string.key_output_format));
119 | if (listPreference != null) {
120 | listPreference.setSummary(listPreference.getEntries()[listPreference.findIndexOfValue(newValue.toString())]);
121 | listPreference.setValue(newValue.toString());
122 | }
123 | break;
124 | }
125 |
126 | return true;
127 | }
128 |
129 | private void setPreviousSelectedAsSummary() {
130 | if (getActivity() != null) {
131 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
132 | String video_resolution = prefs.getString("key_video_resolution", null);
133 | boolean audio_enabled = prefs.getBoolean("key_record_audio", true);
134 | String audio_source = prefs.getString("key_audio_source", null);
135 | String video_encoder = prefs.getString("key_video_encoder", null);
136 | String video_frame_rate = prefs.getString("key_video_fps", null);
137 | String video_bit_rate = prefs.getString("key_video_bitrate", null);
138 | String output_format = prefs.getString("key_output_format", null);
139 |
140 | /*Record Audio Prefs*/
141 | key_record_audio.setChecked(audio_enabled);
142 |
143 | /*Audio Source Prefs*/
144 | if (audio_source != null) {
145 | int index = key_audio_source.findIndexOfValue(audio_source);
146 | key_audio_source.setSummary(key_audio_source.getEntries()[index]);
147 |
148 | } else {
149 | String defaultSummary = PreferenceManager.getDefaultSharedPreferences(key_audio_source.getContext()).getString(key_audio_source.getKey(), "");
150 | key_audio_source.setSummary(defaultSummary);
151 | }
152 |
153 | /*Video Encoder Prefs*/
154 | if (video_encoder != null) {
155 | int index = key_video_encoder.findIndexOfValue(video_encoder);
156 | key_video_encoder.setSummary(key_video_encoder.getEntries()[index]);
157 |
158 | } else {
159 | String defaultSummary = PreferenceManager.getDefaultSharedPreferences(key_video_encoder.getContext()).getString(key_video_encoder.getKey(), "");
160 | key_video_encoder.setSummary(defaultSummary);
161 | }
162 |
163 | /*Video Resolution Prefs*/
164 | if (video_resolution != null) {
165 | int index = key_video_resolution.findIndexOfValue(video_resolution);
166 | key_video_resolution.setSummary(key_video_resolution.getEntries()[index]);
167 |
168 | } else {
169 | String defaultSummary = PreferenceManager.getDefaultSharedPreferences(key_video_resolution.getContext()).getString(key_video_resolution.getKey(), "");
170 | key_video_resolution.setSummary(defaultSummary);
171 | }
172 |
173 | /*Video Frame Rate Prefs*/
174 | if (video_frame_rate != null) {
175 | int index = key_video_fps.findIndexOfValue(video_frame_rate);
176 | key_video_fps.setSummary(key_video_fps.getEntries()[index]);
177 |
178 | } else {
179 | String defaultSummary = PreferenceManager.getDefaultSharedPreferences(key_video_fps.getContext()).getString(key_video_fps.getKey(), "");
180 | key_video_fps.setSummary(defaultSummary);
181 | }
182 |
183 | /*Video Bit Rate Prefs*/
184 | if (video_bit_rate != null) {
185 | int index = key_video_bitrate.findIndexOfValue(video_bit_rate);
186 | key_video_bitrate.setSummary(key_video_bitrate.getEntries()[index]);
187 |
188 | } else {
189 | String defaultSummary = PreferenceManager.getDefaultSharedPreferences(key_video_bitrate.getContext()).getString(key_video_bitrate.getKey(), "");
190 | key_video_bitrate.setSummary(defaultSummary);
191 | }
192 |
193 | /*Output Format Prefs*/
194 | if (output_format != null) {
195 | int index = key_output_format.findIndexOfValue(output_format);
196 | key_output_format.setSummary(key_output_format.getEntries()[index]);
197 |
198 | } else {
199 | String defaultSummary = PreferenceManager.getDefaultSharedPreferences(key_output_format.getContext()).getString(key_output_format.getKey(), "");
200 | key_output_format.setSummary(defaultSummary);
201 | }
202 |
203 | }
204 |
205 | }
206 | }
207 |
208 | @Override
209 | public boolean onOptionsItemSelected(MenuItem item) {
210 | if (item.getItemId() == android.R.id.home) {
211 | onBackPressed();
212 | }
213 | return super.onOptionsItemSelected(item);
214 | }
215 |
216 | }
217 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HBiSoft/HBRecorder/b8c941de636b77376b818bebc61fd87ac4c03975/app/src/main/res/drawable/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple_effect.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 | -
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
22 |
23 |
31 |
32 |
39 |
40 |
46 |
47 |
48 |
49 |
50 |
60 |
61 |
71 |
72 |
82 |
83 |
93 |
94 |
100 |
101 |
109 |
110 |
116 |
117 |
118 |
119 |
120 |
125 |
126 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HBiSoft/HBRecorder/b8c941de636b77376b818bebc61fd87ac4c03975/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HBiSoft/HBRecorder/b8c941de636b77376b818bebc61fd87ac4c03975/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HBiSoft/HBRecorder/b8c941de636b77376b818bebc61fd87ac4c03975/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HBiSoft/HBRecorder/b8c941de636b77376b818bebc61fd87ac4c03975/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HBiSoft/HBRecorder/b8c941de636b77376b818bebc61fd87ac4c03975/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - 426x240
5 | - 640x360
6 | - 854x480
7 | - 1280x720
8 | - 1920x1080
9 | - Default
10 |
11 |
12 | - 60 FPS
13 | - 50 FPS
14 | - 48 FPS
15 | - 30 FPS
16 | - 25 FPS
17 | - 24 FPS
18 | - Default
19 |
20 |
21 | - Auto
22 | - 12 Mbps
23 | - 8 Mbps
24 | - 7.5 Mbps
25 | - 5 Mbps
26 | - 4 Mbps
27 | - 2.5 Mbps
28 | - 1.5 Mbps
29 | - 1 Mbps
30 |
31 |
32 |
33 | - DEFAULT
34 | - MPEG_4
35 | - THREE_GPP
36 | - WEBM
37 |
38 |
39 |
40 | - DEFAULT
41 | - H264
42 | - H263
43 | - HEVC
44 | - MPEG_4_SP
45 | - VP8
46 |
47 |
48 |
49 | - DEFAULT
50 | - CAMCODER
51 | - MIC
52 |
53 |
54 |
55 | - 0
56 | - 1
57 | - 2
58 |
59 |
60 |
61 | - 0
62 | - 1
63 | - 2
64 | - 3
65 | - 4
66 | - 5
67 | - 6
68 | - 7
69 | - 8
70 | - 9
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @android:color/holo_red_light
4 | @android:color/holo_red_dark
5 | #e70041
6 | #ffffff
7 | #303030
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | HBRecorderExample
3 | START
4 | STOP
5 | Record Audio
6 | High Definition
7 | Standard Definition
8 | Max File Size (k)
9 |
10 | Settings
11 | Settings
12 | Resolution
13 | Record Audio
14 |
15 | Specify video size
16 | Should audio be recorded
17 |
18 |
19 | key_record_audio
20 | key_video_resolution
21 | key_video_fps
22 | key_video_bitrate
23 | key_show_notification
24 | key_video_encoder
25 | key_audio_source
26 | key_output_format
27 | QUICK SETTINGS
28 | USE CUSTOM SETTINGS
29 | or
30 | Enter max file size
31 | Drag down to stop the recording
32 | Recording your screen
33 | Some settings are not supported by your device
34 | The file reached the designated max size
35 | HBRecorderOnError - See Log
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_descriptor.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/pref_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
18 |
19 |
20 |
21 |
22 |
23 |
31 |
32 |
40 |
41 |
49 |
50 |
58 |
59 |
60 |
61 |
62 |
63 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/test/java/com/hbisoft/hbrecorderexample/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorderexample;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | mavenCentral()
7 | }
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:8.2.0'
10 |
11 | // NOTE: Do not place your application dependencies here; they belong
12 | // in the individual module build.gradle files
13 | }
14 | }
15 |
16 | allprojects {
17 | repositories {
18 | google()
19 | mavenCentral()
20 | }
21 | }
22 |
23 | task clean(type: Delete) {
24 | delete rootProject.buildDir
25 | }
26 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | android.defaults.buildfeatures.buildconfig=true
10 | android.enableJetifier=true
11 | android.nonFinalResIds=false
12 | android.nonTransitiveRClass=false
13 | android.useAndroidX=true
14 | org.gradle.jvmargs=-Xmx1536m
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 |
21 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HBiSoft/HBRecorder/b8c941de636b77376b818bebc61fd87ac4c03975/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/hbrecorder/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/hbrecorder/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'maven-publish'
3 |
4 | android {
5 | compileSdk 34
6 |
7 | defaultConfig {
8 | minSdk 17
9 | targetSdk 34
10 |
11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
12 |
13 | }
14 |
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 | namespace 'com.hbisoft.hbrecorder'
22 |
23 | publishing {
24 | singleVariant("release") {
25 | withSourcesJar()
26 | withJavadocJar()
27 | }
28 | }
29 |
30 | }
31 |
32 | dependencies {
33 | implementation fileTree(dir: 'libs', include: ['*.jar'])
34 |
35 | implementation 'androidx.appcompat:appcompat:1.6.1'
36 | testImplementation 'junit:junit:4.13.2'
37 | androidTestImplementation 'androidx.test:runner:1.5.2'
38 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
39 | }
40 |
41 | afterEvaluate {
42 | publishing {
43 | publications {
44 | // Creates a Maven publication called "release".
45 | release(MavenPublication) {
46 | from components.release
47 | groupId = 'com.github.HBiSoft'
48 | artifactId = 'HBRecorder'
49 | version = '3.0.3'
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/hbrecorder/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 |
--------------------------------------------------------------------------------
/hbrecorder/src/androidTest/java/com/hbisoft/hbrecorder/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import android.content.Context;
4 | import androidx.test.InstrumentationRegistry;
5 | import androidx.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.hbisoft.hbrecorder.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/hbrecorder/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/hbrecorder/src/main/java/com/hbisoft/hbrecorder/Constants.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | public class Constants {
4 | public final static String MAX_FILE_SIZE_KEY = "maxFileSize";
5 | public final static String ERROR_REASON_KEY = "errorReason";
6 | public final static String ERROR_KEY = "error";
7 | public final static String ON_COMPLETE_KEY = "onComplete";
8 | public final static String ON_START_KEY = "onStart";
9 | public final static String ON_COMPLETE = "Uri was passed";
10 | public final static String ON_PAUSE_KEY = "onPause";
11 | public final static String ON_RESUME_KEY = "onResume";
12 | public final static String ON_PAUSE = "Paused";
13 | public final static String ON_RESUME = "Resumed";
14 | public final static int SETTINGS_ERROR = 38;
15 | public final static int MAX_FILE_SIZE_REACHED_ERROR = 48;
16 | public final static int GENERAL_ERROR = 100;
17 | public final static int ON_START = 111;
18 | public final static int NO_SPECIFIED_MAX_SIZE = 0;
19 | }
20 |
--------------------------------------------------------------------------------
/hbrecorder/src/main/java/com/hbisoft/hbrecorder/Countdown.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import java.util.Timer;
4 | import java.util.TimerTask;
5 |
6 | public abstract class Countdown extends Timer {
7 | private long totalTime, interval, delay;
8 | private TimerTask task;
9 | private long startTime = -1;
10 | private boolean restart = false, wasCancelled = false, wasStarted = false;
11 |
12 | public Countdown(long totalTime, long interval) {
13 | this(totalTime, interval, 0);
14 | }
15 |
16 | public Countdown(long totalTime, long interval, long delay) {
17 | super("PreciseCountdown", true);
18 | this.delay = delay;
19 | this.interval = interval;
20 | this.totalTime = totalTime;
21 | this.task = getTask(totalTime);
22 | }
23 |
24 | public void start() {
25 | wasStarted = true;
26 | this.schedule(task, delay, interval);
27 | }
28 |
29 | public void stop() {
30 | onStopCalled();
31 | this.wasCancelled = true;
32 | this.task.cancel();
33 | dispose();
34 | }
35 |
36 | // Call this when there's no further use for this timer
37 | public void dispose(){
38 | cancel();
39 | purge();
40 | }
41 |
42 | private TimerTask getTask(final long totalTime) {
43 | return new TimerTask() {
44 |
45 | @Override
46 | public void run() {
47 | long timeLeft;
48 | if (startTime < 0 || restart) {
49 | startTime = scheduledExecutionTime();
50 | timeLeft = totalTime;
51 | restart = false;
52 | } else {
53 | timeLeft = totalTime - (scheduledExecutionTime() - startTime);
54 |
55 | if (timeLeft <= 0) {
56 | this.cancel();
57 | startTime = -1;
58 | onFinished();
59 | return;
60 | }
61 | }
62 |
63 | onTick(timeLeft);
64 | }
65 | };
66 | }
67 |
68 | public abstract void onTick(long timeLeft);
69 | public abstract void onFinished();
70 | public abstract void onStopCalled();
71 | }
72 |
--------------------------------------------------------------------------------
/hbrecorder/src/main/java/com/hbisoft/hbrecorder/FileObserver.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import java.io.File;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 | import java.util.Stack;
7 |
8 | import android.os.Handler;
9 | import android.os.Looper;
10 |
11 |
12 | class FileObserver extends android.os.FileObserver {
13 |
14 | private List mObservers;
15 | private final String mPath;
16 | private final int mMask;
17 | private final MyListener ml;
18 |
19 | FileObserver(String path, MyListener ml) {
20 | super(path, ALL_EVENTS);
21 | mPath = path;
22 | mMask = ALL_EVENTS;
23 | this.ml = ml;
24 | }
25 |
26 |
27 | @Override
28 | public void startWatching() {
29 | if (mObservers != null) return;
30 |
31 | mObservers = new ArrayList<>();
32 | Stack stack = new Stack<>();
33 | stack.push(mPath);
34 |
35 | while (!stack.isEmpty()) {
36 | String parent = stack.pop();
37 | mObservers.add(new SingleFileObserver(parent, mMask));
38 | File path = new File(parent);
39 | File[] files = path.listFiles();
40 | if (null == files) continue;
41 |
42 | for (File f : files) {
43 | if (f.isDirectory() && !f.getName().equals(".") && !f.getName().equals("..")) {
44 | stack.push(f.getPath());
45 | }
46 | }
47 | }
48 |
49 | for (SingleFileObserver sfo : mObservers) {
50 | sfo.startWatching();
51 | }
52 | }
53 |
54 | @Override
55 | public void stopWatching() {
56 | if (mObservers == null) return;
57 |
58 | for (SingleFileObserver sfo : mObservers) {
59 | sfo.stopWatching();
60 | }
61 | mObservers.clear();
62 | mObservers = null;
63 | }
64 |
65 | @Override
66 | public void onEvent(int event, final String path) {
67 | if (event == android.os.FileObserver.CLOSE_WRITE) {
68 | new Handler(Looper.getMainLooper()).post(new Runnable() {
69 | public void run() {
70 | ml.onCompleteCallback();
71 | }
72 | });
73 | }
74 | }
75 |
76 | class SingleFileObserver extends android.os.FileObserver {
77 | final String mPath;
78 |
79 |
80 | SingleFileObserver(String path, int mask) {
81 | super(path, mask);
82 | mPath = path;
83 | }
84 |
85 | @Override
86 | public void onEvent(int event, String path) {
87 | String newPath = mPath + "/" + path;
88 | FileObserver.this.onEvent(event, newPath);
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/hbrecorder/src/main/java/com/hbisoft/hbrecorder/HBRecorder.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import android.app.Activity;
4 | import android.app.ActivityManager;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.content.res.Resources;
8 | import android.graphics.Bitmap;
9 | import android.graphics.BitmapFactory;
10 | import android.media.AudioManager;
11 | import android.net.Uri;
12 | import android.os.Build;
13 | import android.os.Bundle;
14 | import android.os.CountDownTimer;
15 | import android.os.Environment;
16 |
17 | import androidx.annotation.DrawableRes;
18 | import androidx.annotation.RequiresApi;
19 |
20 | import android.os.Handler;
21 | import android.os.Looper;
22 | import android.os.ResultReceiver;
23 | import android.util.DisplayMetrics;
24 | import android.util.Log;
25 |
26 |
27 | import java.io.ByteArrayOutputStream;
28 | import java.io.File;
29 |
30 | import static com.hbisoft.hbrecorder.Constants.ERROR_KEY;
31 | import static com.hbisoft.hbrecorder.Constants.ERROR_REASON_KEY;
32 | import static com.hbisoft.hbrecorder.Constants.GENERAL_ERROR;
33 | import static com.hbisoft.hbrecorder.Constants.MAX_FILE_SIZE_KEY;
34 | import static com.hbisoft.hbrecorder.Constants.NO_SPECIFIED_MAX_SIZE;
35 | import static com.hbisoft.hbrecorder.Constants.ON_COMPLETE_KEY;
36 | import static com.hbisoft.hbrecorder.Constants.ON_START_KEY;
37 | import static com.hbisoft.hbrecorder.Constants.ON_PAUSE_KEY;
38 | import static com.hbisoft.hbrecorder.Constants.ON_RESUME_KEY;
39 |
40 | /**
41 | * Created by HBiSoft on 13 Aug 2019
42 | * Copyright (c) 2019 . All rights reserved.
43 | */
44 |
45 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
46 | public class HBRecorder implements MyListener {
47 | private int mScreenWidth;
48 | private int mScreenHeight;
49 | private int mScreenDensity;
50 | private final Context context;
51 | private int resultCode;
52 | private boolean isAudioEnabled = true;
53 | private boolean isVideoHDEnabled = true;
54 | private String outputPath;
55 | private String fileName;
56 | private String notificationTitle;
57 | private String notificationDescription;
58 | private String notificationButtonText;
59 | private int audioBitrate = 0;
60 | private int audioSamplingRate = 0;
61 | private FileObserver observer;
62 | private final HBRecorderListener hbRecorderListener;
63 | private byte[] byteArray;
64 | private int vectorDrawable = 0;
65 | private String audioSource = "MIC";
66 | private String videoEncoder = "DEFAULT";
67 | private boolean enableCustomSettings = false;
68 | private int videoFrameRate = 30;
69 | private int videoBitrate = 40000000;
70 | private String outputFormat = "DEFAULT";
71 | private int orientation;
72 | private long maxFileSize = NO_SPECIFIED_MAX_SIZE; // Default no max size
73 | boolean wasOnErrorCalled = false;
74 | Intent service;
75 | boolean isPaused = false;
76 | boolean isMaxDurationSet = false;
77 | int maxDuration = 0;
78 |
79 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
80 | public HBRecorder(Context context, HBRecorderListener listener) {
81 | this.context = context.getApplicationContext();
82 | this.hbRecorderListener = listener;
83 | setScreenDensity();
84 | }
85 |
86 | public void setOrientationHint(int orientationInDegrees){
87 | orientation = orientationInDegrees;
88 | }
89 |
90 | /*Set output path*/
91 | public void setOutputPath(String path) {
92 | outputPath = path;
93 | }
94 |
95 | Uri mUri;
96 | boolean mWasUriSet = false;
97 | @RequiresApi(api = Build.VERSION_CODES.Q)
98 | public void setOutputUri(Uri uri){
99 | mWasUriSet = true;
100 | mUri = uri;
101 | }
102 |
103 | // WILL IMPLEMENT THIS AT A LATER STAGE
104 | // DEVELOPERS ARE WELCOME TO LOOK AT THIS AND CREATE A PULL REQUEST
105 | /*Mute microphone*/
106 | /*public void setMicMuted(boolean state){
107 | if (context!=null) {
108 | try {
109 | ((AudioManager)context.getSystemService(Context.AUDIO_SERVICE)).setStreamMute(AudioManager.STREAM_SYSTEM,true);
110 |
111 | AudioManager myAudioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
112 |
113 | // get the working mode and keep it
114 | int workingAudioMode = myAudioManager.getMode();
115 |
116 | myAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
117 |
118 | // change mic state only if needed
119 | if (myAudioManager.isMicrophoneMute() != state) {
120 | myAudioManager.setMicrophoneMute(state);
121 | }
122 |
123 | // set back the original working mode
124 | myAudioManager.setMode(workingAudioMode);
125 | }catch (Exception e){
126 | Log.e("HBRecorder", "Muting mic failed with the following exception:");
127 | e.printStackTrace();
128 | }
129 |
130 | }
131 | }*/
132 |
133 | /*Set max duration in seconds */
134 | public void setMaxDuration(int seconds){
135 | isMaxDurationSet = true;
136 | maxDuration = seconds * 1000;
137 | }
138 |
139 | /*Set max file size in kb*/
140 | public void setMaxFileSize(long fileSize) {
141 | maxFileSize = fileSize;
142 | }
143 |
144 | public boolean wasUriSet(){
145 | return mWasUriSet;
146 | }
147 |
148 | /*Set file name*/
149 | public void setFileName(String fileName) {
150 | this.fileName = fileName;
151 | }
152 |
153 | /*Set audio bitrate*/
154 | public void setAudioBitrate(int audioBitrate) {
155 | this.audioBitrate = audioBitrate;
156 |
157 | }
158 |
159 | /*Set audio sample rate*/
160 | public void setAudioSamplingRate(int audioSamplingRate) {
161 | this.audioSamplingRate = audioSamplingRate;
162 | }
163 |
164 | /*Enable/Disable audio*/
165 | public void isAudioEnabled(boolean bool) {
166 | this.isAudioEnabled = bool;
167 | }
168 |
169 | /*Set Audio Source*/
170 | //MUST BE ONE OF THE FOLLOWING - https://developer.android.com/reference/android/media/MediaRecorder.AudioSource.html
171 | public void setAudioSource(String source){
172 | audioSource = source;
173 |
174 | }
175 |
176 | /*Enable/Disable HD recording*/
177 | public void recordHDVideo(boolean bool) {
178 | this.isVideoHDEnabled = bool;
179 | }
180 |
181 | /*Set Video Encoder*/
182 | //MUST BE ONE OF THE FOLLOWING - https://developer.android.com/reference/android/media/MediaRecorder.VideoEncoder.html
183 | public void setVideoEncoder(String encoder){
184 | videoEncoder = encoder;
185 |
186 | }
187 |
188 | //Enable Custom Settings
189 | public void enableCustomSettings(){
190 | enableCustomSettings = true;
191 |
192 | }
193 |
194 | //Set Video Frame Rate
195 | public void setVideoFrameRate(int fps){
196 | videoFrameRate = fps;
197 | }
198 |
199 | //Set Video BitRate
200 | public void setVideoBitrate(int bitrate){
201 | videoBitrate = bitrate;
202 | }
203 |
204 | //Set Output Format
205 | //MUST BE ONE OF THE FOLLOWING - https://developer.android.com/reference/android/media/MediaRecorder.OutputFormat.html
206 | public void setOutputFormat(String format){
207 | outputFormat = format;
208 | }
209 |
210 | // Set screen densityDpi
211 | private void setScreenDensity() {
212 | DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
213 | mScreenDensity = metrics.densityDpi;
214 | }
215 |
216 | //Get default width
217 | public int getDefaultWidth(){
218 | HBRecorderCodecInfo hbRecorderCodecInfo = new HBRecorderCodecInfo();
219 | hbRecorderCodecInfo.setContext(context);
220 | return hbRecorderCodecInfo.getMaxSupportedWidth();
221 | }
222 |
223 | //Get default height
224 | public int getDefaultHeight(){
225 | HBRecorderCodecInfo hbRecorderCodecInfo = new HBRecorderCodecInfo();
226 | hbRecorderCodecInfo.setContext(context);
227 | return hbRecorderCodecInfo.getMaxSupportedHeight();
228 | }
229 |
230 | //Set Custom Dimensions (NOTE - YOUR DEVICE MIGHT NOT SUPPORT THE SIZE YOU PASS IT)
231 | public void setScreenDimensions(int heightInPX, int widthInPX){
232 | mScreenHeight = heightInPX;
233 | mScreenWidth = widthInPX;
234 | }
235 |
236 | /*Get file path including file name and extension*/
237 | public String getFilePath() {
238 | return ScreenRecordService.getFilePath();
239 | }
240 |
241 | /*Get file name and extension*/
242 | public String getFileName() {
243 | return ScreenRecordService.getFileName();
244 | }
245 |
246 | /*Start screen recording*/
247 | public void startScreenRecording(Intent data, int resultCode) {
248 | this.resultCode = resultCode;
249 | startService(data);
250 | }
251 |
252 | /*Stop screen recording*/
253 | public void stopScreenRecording() {
254 | Intent service = new Intent(context, ScreenRecordService.class);
255 | context.stopService(service);
256 | }
257 |
258 | /*Pause screen recording*/
259 | @RequiresApi(api = Build.VERSION_CODES.N)
260 | public void pauseScreenRecording(){
261 | if (service != null){
262 | isPaused = true;
263 | service.setAction("pause");
264 | context.startService(service);
265 | }
266 | }
267 |
268 | /*Pause screen recording*/
269 | @RequiresApi(api = Build.VERSION_CODES.N)
270 | public void resumeScreenRecording(){
271 | if (service != null){
272 | isPaused = false;
273 | service.setAction("resume");
274 | context.startService(service);
275 | }
276 | }
277 |
278 | /*Check if video is paused*/
279 | public boolean isRecordingPaused(){
280 | return isPaused;
281 | }
282 |
283 | /*Check if recording is in progress*/
284 | public boolean isBusyRecording() {
285 | ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
286 | if (manager != null) {
287 | for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
288 | if (ScreenRecordService.class.getName().equals(service.service.getClassName())) {
289 | return true;
290 | }
291 | }
292 | }
293 | return false;
294 | }
295 |
296 | /*Change notification icon Drawable*/
297 | public void setNotificationSmallIcon(@DrawableRes int drawable) {
298 | Bitmap icon = BitmapFactory.decodeResource(context.getResources(), drawable);
299 | ByteArrayOutputStream stream = new ByteArrayOutputStream();
300 | icon.compress(Bitmap.CompressFormat.PNG, 100, stream);
301 | byteArray = stream.toByteArray();
302 | }
303 |
304 | /*Change notification icon using Vector Drawable*/
305 | public void setNotificationSmallIconVector(@DrawableRes int VectorDrawable) {
306 | vectorDrawable = VectorDrawable;
307 | }
308 |
309 | /*Change notification icon using byte[]*/
310 | public void setNotificationSmallIcon(byte[] bytes) {
311 | byteArray = bytes;
312 | }
313 |
314 | /*Set notification title*/
315 | public void setNotificationTitle(String Title) {
316 | notificationTitle = Title;
317 | }
318 |
319 | /*Set notification description*/
320 | public void setNotificationDescription(String Description) {
321 | notificationDescription = Description;
322 | }
323 |
324 | public void setNotificationButtonText(String string){
325 | notificationButtonText = string;
326 | }
327 |
328 | /*Start recording service*/
329 | private void startService(Intent data) {
330 | try {
331 | if (!mWasUriSet) {
332 | if (outputPath != null) {
333 | File file = new File(outputPath);
334 | String parent = file.getParent();
335 | observer = new FileObserver(parent, HBRecorder.this);
336 | } else {
337 | observer = new FileObserver(String.valueOf(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)), HBRecorder.this);
338 | }
339 | observer.startWatching();
340 | }
341 |
342 | service = new Intent(context, ScreenRecordService.class);
343 | if (mWasUriSet) {
344 | service.putExtra("mUri", mUri.toString());
345 | }
346 | service.putExtra("code", resultCode);
347 | service.putExtra("data", data);
348 | service.putExtra("audio", isAudioEnabled);
349 | service.putExtra("width", mScreenWidth);
350 | service.putExtra("height", mScreenHeight);
351 | service.putExtra("density", mScreenDensity);
352 | service.putExtra("quality", isVideoHDEnabled);
353 | service.putExtra("path", outputPath);
354 | service.putExtra("fileName", fileName);
355 | service.putExtra("orientation", orientation);
356 | service.putExtra("audioBitrate", audioBitrate);
357 | service.putExtra("audioSamplingRate", audioSamplingRate);
358 | service.putExtra("notificationSmallBitmap", byteArray);
359 | service.putExtra("notificationSmallVector", vectorDrawable);
360 | service.putExtra("notificationTitle", notificationTitle);
361 | service.putExtra("notificationDescription", notificationDescription);
362 | service.putExtra("notificationButtonText", notificationButtonText);
363 | service.putExtra("enableCustomSettings", enableCustomSettings);
364 | service.putExtra("audioSource",audioSource);
365 | service.putExtra("videoEncoder", videoEncoder);
366 |
367 | service.putExtra("videoFrameRate", videoFrameRate);
368 | service.putExtra("videoBitrate", videoBitrate);
369 | service.putExtra("outputFormat", outputFormat);
370 | service.putExtra(ScreenRecordService.BUNDLED_LISTENER, new ResultReceiver(new Handler()) {
371 | @Override
372 | protected void onReceiveResult(int resultCode, Bundle resultData) {
373 | super.onReceiveResult(resultCode, resultData);
374 | if (resultCode == Activity.RESULT_OK) {
375 | String errorListener = resultData.getString(ERROR_REASON_KEY);
376 | String onComplete = resultData.getString(ON_COMPLETE_KEY);
377 | int onStartCode = resultData.getInt(ON_START_KEY);
378 | int errorCode = resultData.getInt(ERROR_KEY);
379 | // There was an error
380 | if (errorListener != null) {
381 | //Stop countdown if it was set
382 | stopCountDown();
383 | if (!mWasUriSet) {
384 | observer.stopWatching();
385 | }
386 | wasOnErrorCalled = true;
387 | if ( errorCode > 0 ) {
388 | hbRecorderListener.HBRecorderOnError(errorCode, errorListener);
389 | } else {
390 | hbRecorderListener.HBRecorderOnError(GENERAL_ERROR, errorListener);
391 | }
392 | try {
393 | Intent mService = new Intent(context, ScreenRecordService.class);
394 | context.stopService(mService);
395 | }catch (Exception e){
396 | // Can be ignored
397 | }
398 |
399 | }
400 | // OnComplete was called
401 | else if (onComplete != null){
402 | //Stop countdown if it was set
403 | stopCountDown();
404 | //OnComplete for when Uri was passed
405 | if (mWasUriSet && !wasOnErrorCalled) {
406 | hbRecorderListener.HBRecorderOnComplete();
407 | }
408 | wasOnErrorCalled = false;
409 | }
410 | // OnStart was called
411 | else if (onStartCode != 0){
412 | hbRecorderListener.HBRecorderOnStart();
413 | //Check if max duration was set and start count down
414 | if (isMaxDurationSet){
415 | startCountdown();
416 | }
417 | }
418 | // OnPause/onResume was called
419 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
420 | String onPause = resultData.getString(ON_PAUSE_KEY);
421 | String onResume = resultData.getString(ON_RESUME_KEY);
422 | if (onPause != null) {
423 | hbRecorderListener.HBRecorderOnPause();
424 | } else if (onResume != null) {
425 | hbRecorderListener.HBRecorderOnResume();
426 | }
427 | }
428 | }
429 | }
430 | });
431 | // Max file size
432 | service.putExtra(MAX_FILE_SIZE_KEY, maxFileSize);
433 | context.startService(service);
434 | }catch (Exception e){
435 | hbRecorderListener.HBRecorderOnError(0, Log.getStackTraceString(e));
436 | }
437 |
438 | }
439 |
440 | /*CountdownTimer for when max duration is set*/
441 | Countdown countDown = null;
442 | private void startCountdown() {
443 | countDown = new Countdown(maxDuration, 1000, 0) {
444 | @Override
445 | public void onTick(long timeLeft) {
446 | // Could add a callback to provide the time to the user
447 | // Will add if users request this
448 | }
449 |
450 | @Override
451 | public void onFinished() {
452 | onTick(0);
453 | // Since the timer is running on a different thread
454 | // UI chances should be called from the UI Thread
455 | new Handler(Looper.getMainLooper()).post(new Runnable() {
456 | @Override
457 | public void run() {
458 | try {
459 | stopScreenRecording();
460 | observer.stopWatching();
461 | hbRecorderListener.HBRecorderOnComplete();
462 | } catch (Exception e){
463 | e.printStackTrace();
464 | }
465 | }
466 | });
467 | }
468 |
469 | @Override
470 | public void onStopCalled() {
471 | // Currently unused, but might be helpful in the future
472 | }
473 | };
474 | countDown.start();
475 | }
476 |
477 | private void stopCountDown(){
478 | if (countDown != null) {
479 | countDown.stop();
480 | }
481 | }
482 |
483 | /*Complete callback method*/
484 | @Override
485 | public void onCompleteCallback() {
486 | observer.stopWatching();
487 | hbRecorderListener.HBRecorderOnComplete();
488 | }
489 | }
490 |
--------------------------------------------------------------------------------
/hbrecorder/src/main/java/com/hbisoft/hbrecorder/HBRecorderCodecInfo.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import android.content.Context;
4 | import android.content.res.Configuration;
5 | import android.media.CamcorderProfile;
6 | import android.media.MediaCodecInfo;
7 | import android.media.MediaCodecList;
8 | import android.os.Build;
9 | import android.util.DisplayMetrics;
10 | import android.util.Range;
11 | import android.view.WindowManager;
12 |
13 | import androidx.annotation.RequiresApi;
14 |
15 | import java.util.ArrayList;
16 | import java.util.HashMap;
17 |
18 | import static android.content.Context.WINDOW_SERVICE;
19 | import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
20 | import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
21 |
22 | /**
23 | * Created by HBiSoft on 13 Aug 2019
24 | * Copyright (c) 2019 . All rights reserved.
25 | */
26 |
27 | public class HBRecorderCodecInfo {
28 |
29 | int getMaxSupportedWidth(){
30 | RecordingInfo recordingInfo = getRecordingInfo();
31 | return recordingInfo.width;
32 | }
33 |
34 | int getMaxSupportedHeight(){
35 | RecordingInfo recordingInfo = getRecordingInfo();
36 | return recordingInfo.height;
37 | }
38 |
39 | private RecordingInfo getRecordingInfo() {
40 | DisplayMetrics displayMetrics = new DisplayMetrics();
41 | WindowManager wm = (WindowManager) context.getSystemService(WINDOW_SERVICE);
42 | wm.getDefaultDisplay().getRealMetrics(displayMetrics);
43 | int displayWidth = displayMetrics.widthPixels;
44 | int displayHeight = displayMetrics.heightPixels;
45 | int displayDensity = displayMetrics.densityDpi;
46 |
47 | Configuration configuration = context.getResources().getConfiguration();
48 | boolean isLandscape = configuration.orientation == ORIENTATION_LANDSCAPE;
49 |
50 | CamcorderProfile camcorderProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
51 | int cameraWidth = camcorderProfile != null ? camcorderProfile.videoFrameWidth : -1;
52 | int cameraHeight = camcorderProfile != null ? camcorderProfile.videoFrameHeight : -1;
53 | int cameraFrameRate = camcorderProfile != null ? camcorderProfile.videoFrameRate : 30;
54 |
55 |
56 | return calculateRecordingInfo(displayWidth, displayHeight, displayDensity, isLandscape,
57 | cameraWidth, cameraHeight, cameraFrameRate, 100);
58 | }
59 |
60 | private Context context;
61 |
62 | public void setContext(Context c) {
63 | context = c;
64 | }
65 |
66 | static final class RecordingInfo {
67 | final int width;
68 | final int height;
69 | final int frameRate;
70 | final int density;
71 |
72 | RecordingInfo(int width, int height, int frameRate, int density) {
73 | this.width = width;
74 | this.height = height;
75 | this.frameRate = frameRate;
76 | this.density = density;
77 | }
78 | }
79 |
80 | static RecordingInfo calculateRecordingInfo(int displayWidth, int displayHeight, int displayDensity, boolean isLandscapeDevice, int cameraWidth, int cameraHeight, int cameraFrameRate, int sizePercentage) {
81 | // Scale the display size before any maximum size calculations.
82 | displayWidth = displayWidth * sizePercentage / 100;
83 | displayHeight = displayHeight * sizePercentage / 100;
84 |
85 | if (cameraWidth == -1 && cameraHeight == -1) {
86 | // No cameras. Fall back to the display size.
87 | return new RecordingInfo(displayWidth, displayHeight, cameraFrameRate, displayDensity);
88 | }
89 |
90 | int frameWidth = isLandscapeDevice ? cameraWidth : cameraHeight;
91 | int frameHeight = isLandscapeDevice ? cameraHeight : cameraWidth;
92 | if (frameWidth >= displayWidth && frameHeight >= displayHeight) {
93 | // Frame can hold the entire display. Use exact values.
94 | return new RecordingInfo(displayWidth, displayHeight, cameraFrameRate, displayDensity);
95 | }
96 |
97 | // Calculate new width or height to preserve aspect ratio.
98 | if (isLandscapeDevice) {
99 | frameWidth = displayWidth * frameHeight / displayHeight;
100 | } else {
101 | frameHeight = displayHeight * frameWidth / displayWidth;
102 | }
103 | return new RecordingInfo(frameWidth, frameHeight, cameraFrameRate, displayDensity);
104 | }
105 |
106 |
107 | // ALL PUBLIC METHODS
108 | // This is for testing purposes only
109 |
110 | // Select the default video encoder
111 | // This will only return the default video encoder, it does not mean that the encoder supports a particular set of parameters
112 | public final MediaCodecInfo selectVideoCodec(final String mimeType) {
113 | MediaCodecInfo result = null;
114 | // get the list of available codecs
115 | final int numCodecs = MediaCodecList.getCodecCount();
116 | for (int i = 0; i < numCodecs; i++) {
117 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
118 | if (!codecInfo.isEncoder()) { // skipp decoder
119 | continue;
120 | }
121 | String[] types = codecInfo.getSupportedTypes();
122 | for (String type : types) {
123 | if (type.equalsIgnoreCase(mimeType)) {
124 | return codecInfo;
125 | }
126 | }
127 | }
128 | return result;
129 | }
130 |
131 | private String selectCodecByMime(String mimeType) {
132 | int numCodecs = MediaCodecList.getCodecCount();
133 | for (int i = 0; i < numCodecs; i++) {
134 | MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
135 | if (!codecInfo.isEncoder()) {
136 | continue;
137 | }
138 | String[] types = codecInfo.getSupportedTypes();
139 | for (String type : types) {
140 | if (type.equalsIgnoreCase(mimeType)) {
141 | return codecInfo.getName();
142 | }
143 | }
144 | }
145 | return "Mime not supported";
146 | }
147 |
148 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
149 | private MediaCodecInfo selectDefaultCodec() {
150 | MediaCodecInfo result = null;
151 | // get the list of available codecs
152 | final int numCodecs = MediaCodecList.getCodecCount();
153 | for (int i = 0; i < numCodecs; i++) {
154 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
155 | if (!codecInfo.isEncoder()) { // skipp decoder
156 | continue;
157 | }
158 | final String[] types = codecInfo.getSupportedTypes();
159 | for (int j = 0; j < types.length; j++) {
160 | if (types[j].contains("video")){
161 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(types[j]);
162 | boolean formatSup = codecCapabilities.isFormatSupported(codecCapabilities.getDefaultFormat());
163 | if (formatSup) {
164 | return codecInfo;
165 | }
166 | }
167 | }
168 | }
169 | return result;
170 | }
171 |
172 | // Get the default video encoder name
173 | // The default one will be returned first
174 | public String getDefaultVideoEncoderName(String mimeType){
175 | String defaultEncoder = "";
176 | try {
177 | defaultEncoder = selectCodecByMime(mimeType);
178 | }catch (Exception e){
179 | e.printStackTrace();
180 | }
181 | return defaultEncoder;
182 | }
183 |
184 | // Get the default video format
185 | // The default one will be returned first
186 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
187 | public String getDefaultVideoFormat(){
188 | String supported = "";
189 | try {
190 | final MediaCodecInfo codecInfo = selectDefaultCodec();
191 | if (codecInfo != null) {
192 | String[] types = codecInfo.getSupportedTypes();
193 | for (String type : types) {
194 | if (type.contains("video")) {
195 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(type);
196 | String result = codecCapabilities.getDefaultFormat().toString();
197 | return returnTypeFromMime(result.substring(result.indexOf("=") + 1, result.indexOf(",")));
198 | }
199 | }
200 | }else {
201 | supported = "null";
202 | }
203 | }catch (Exception e){
204 | e.printStackTrace();
205 | }
206 | return supported;
207 | }
208 |
209 | private String returnTypeFromMime(String mimeType){
210 | switch (mimeType) {
211 | case "video/MP2T":
212 | return "MPEG_2_TS";
213 | case "video/mp4v-es":
214 | return "MPEG_4";
215 | case "video/mp4v":
216 | return "MPEG_4";
217 | case "video/mp4":
218 | return "MPEG_4";
219 | case "video/avc":
220 | return "MPEG_4";
221 | case "video/3gpp":
222 | return "THREE_GPP";
223 | case "video/webm":
224 | return "WEBM";
225 | case "video/x-vnd.on2.vp8":
226 | return "WEBM";
227 | }
228 | return "";
229 | }
230 |
231 | // Example usage - isSizeAndFramerateSupported(hbrecorder.getWidth(), hbrecorder.getHeight(), 30, "video/mp4", ORIENTATION_PORTRAIT);
232 | // int width - The width of the view to be recorder
233 | // int height - The height of the view to be recorder
234 | // String mimeType - for ex. video/mp4
235 | // int orientation - ORIENTATION_PORTRAIT or ORIENTATION_LANDSCAPE
236 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
237 | public boolean isSizeAndFramerateSupported(int width, int height, int fps, String mimeType, int orientation){
238 | boolean supported = false;
239 | try {
240 | final MediaCodecInfo codecInfo = selectVideoCodec(mimeType);
241 | String[] types = codecInfo.getSupportedTypes();
242 | for (String type : types) {
243 | if (type.contains("video")) {
244 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(type);
245 | MediaCodecInfo.VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
246 |
247 | // Flip around the width and height in ORIENTATION_PORTRAIT because android's default orientation is ORIENTATION_LANDSCAPE
248 | if (ORIENTATION_PORTRAIT == orientation) {
249 | supported = videoCapabilities.areSizeAndRateSupported(height, width, fps);
250 | }else {
251 | supported = videoCapabilities.areSizeAndRateSupported(width, height, fps);
252 | }
253 | }
254 | }
255 | }catch (Exception e){
256 | e.printStackTrace();
257 | }
258 | return supported;
259 | }
260 |
261 | public boolean isMimeTypeSupported(String mimeType){
262 | try {
263 | final MediaCodecInfo codecInfo = selectVideoCodec(mimeType);
264 | String[] types = codecInfo.getSupportedTypes();
265 | for (String type : types) {
266 | if (type.contains("video")) {
267 | //ignore
268 | }
269 | }
270 | }catch (Exception e){
271 | return false;
272 | }
273 | return true;
274 | }
275 |
276 | // Check if a particular size is supported
277 | // Provide width and height in Portrait mode, for example. isSizeSupported(1080, 1920, "video/mp4");
278 | // We do this because android's default orientation is landscape, so we have to flip around the width and height
279 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
280 | public boolean isSizeSupported(int width, int height, String mimeType){
281 | boolean supported = false;
282 | try {
283 | final MediaCodecInfo codecInfo = selectVideoCodec(mimeType);
284 | String[] types = codecInfo.getSupportedTypes();
285 | for (String type : types) {
286 | if (type.contains("video")) {
287 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(type);
288 | MediaCodecInfo.VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
289 | supported = videoCapabilities.isSizeSupported(height, width);
290 | }
291 | }
292 | }catch (Exception e){
293 | e.printStackTrace();
294 | }
295 | return supported;
296 | }
297 |
298 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
299 | public double getMaxSupportedFrameRate(int width, int height, String mimeType){
300 | double maxFPS = 0;
301 | try {
302 | final MediaCodecInfo codecInfo = selectVideoCodec(mimeType);
303 | String[] types = codecInfo.getSupportedTypes();
304 | for (String type : types) {
305 | if (type.contains("video")) {
306 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(type);
307 | MediaCodecInfo.VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
308 | Range bit = videoCapabilities.getSupportedFrameRatesFor(height, width);
309 | maxFPS = bit.getUpper();
310 | }
311 | }
312 | }catch (Exception e){
313 | e.printStackTrace();
314 | }
315 | return maxFPS;
316 | }
317 |
318 | // Get the max supported bitrate for a particular mime type
319 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
320 | public int getMaxSupportedBitrate(String mimeType){
321 | int bitrate = 0;
322 | try {
323 | final MediaCodecInfo codecInfo = selectVideoCodec(mimeType);
324 | String[] types = codecInfo.getSupportedTypes();
325 | for (String type : types) {
326 | if (type.contains("video")) {
327 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(type);
328 | MediaCodecInfo.VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
329 | Range bit = videoCapabilities.getBitrateRange();
330 |
331 | bitrate = bit.getUpper();
332 |
333 | }
334 | }
335 | }catch (Exception e){
336 | e.printStackTrace();
337 | }
338 | return bitrate;
339 | }
340 |
341 | // Get supported video formats
342 | ArrayList supportedVideoFormats = new ArrayList<>();
343 | public ArrayList getSupportedVideoFormats(){
344 | String[] allFormats = {"video/MP2T", "video/mp4v-es", "video/m4v", "video/mp4", "video/avc", "video/3gpp", "video/webm", "video/x-vnd.on2.vp8"};
345 |
346 | for (String allFormat : allFormats) {
347 | checkSupportedVideoFormats(allFormat);
348 | }
349 | return supportedVideoFormats;
350 | }
351 | private void checkSupportedVideoFormats(String mimeType){
352 | // get the list of available codecs
353 | final int numCodecs = MediaCodecList.getCodecCount();
354 | LOOP:
355 | for (int i = 0; i < numCodecs; i++) {
356 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
357 | if (!codecInfo.isEncoder()) { // skipp decoder
358 | continue;
359 | }
360 | final String[] types = codecInfo.getSupportedTypes();
361 | for (int j = 0; j < types.length; j++) {
362 | if (types[j].contains("video")){
363 | switch (mimeType) {
364 | case "video/MP2T":
365 | supportedVideoFormats.add("MPEG_2_TS");
366 | break LOOP;
367 | case "video/mp4v-es":
368 | if(!supportedVideoFormats.contains("MPEG_4")) {
369 | supportedVideoFormats.add("MPEG_4");
370 | }
371 | break LOOP;
372 | case "video/mp4v":
373 | if(!supportedVideoFormats.contains("MPEG_4")) {
374 | supportedVideoFormats.add("MPEG_4");
375 | }
376 | break LOOP;
377 | case "video/mp4":
378 | if(!supportedVideoFormats.contains("MPEG_4")) {
379 | supportedVideoFormats.add("MPEG_4");
380 | }
381 | break LOOP;
382 | case "video/avc":
383 | if(!supportedVideoFormats.contains("MPEG_4")) {
384 | supportedVideoFormats.add("MPEG_4");
385 | }
386 | break LOOP;
387 | case "video/3gpp":
388 | supportedVideoFormats.add("THREE_GPP");
389 | break LOOP;
390 |
391 | case "video/webm":
392 | if(!supportedVideoFormats.contains("WEBM")) {
393 | supportedVideoFormats.add("WEBM");
394 | }
395 | break LOOP;
396 | case "video/video/x-vnd.on2.vp8":
397 | if(!supportedVideoFormats.contains("WEBM")) {
398 | supportedVideoFormats.add("WEBM");
399 | }
400 | break LOOP;
401 |
402 | }
403 | }
404 | }
405 | }
406 | }
407 |
408 | // Get supported audio formats
409 | ArrayList supportedAudioFormats = new ArrayList<>();
410 | public ArrayList getSupportedAudioFormats(){
411 | String[] allFormats = {"audio/amr_nb", "audio/amr_wb", "audio/x-hx-aac-adts", "audio/ogg"};
412 |
413 | for (String allFormat : allFormats) {
414 | checkSupportedAudioFormats(allFormat);
415 | }
416 | return supportedAudioFormats;
417 | }
418 | private void checkSupportedAudioFormats(String mimeType){
419 | // get the list of available codecs
420 | final int numCodecs = MediaCodecList.getCodecCount();
421 | LOOP:
422 | for (int i = 0; i < numCodecs; i++) {
423 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
424 | if (!codecInfo.isEncoder()) { // skipp decoder
425 | continue;
426 | }
427 | final String[] types = codecInfo.getSupportedTypes();
428 | label:
429 | for (int j = 0; j < types.length; j++) {
430 | if (types[j].contains("audio")){
431 | switch (mimeType) {
432 | case "audio/amr_nb":
433 | supportedAudioFormats.add("AMR_NB");
434 | break LOOP;
435 | case "audio/amr_wb":
436 | supportedAudioFormats.add("AMR_WB");
437 | break LOOP;
438 | case "audio/x-hx-aac-adts":
439 | supportedAudioFormats.add("AAC_ADTS");
440 | break LOOP;
441 | case "audio/ogg":
442 | supportedAudioFormats.add("OGG");
443 | break LOOP;
444 | }
445 | }
446 | }
447 | }
448 | }
449 |
450 | // Get supported video mime types
451 | HashMap mVideoMap= new HashMap<>();
452 | public HashMap getSupportedVideoMimeTypes(){
453 | checkIfSupportedVideoMimeTypes();
454 | return mVideoMap;
455 | }
456 | private void checkIfSupportedVideoMimeTypes(){
457 | // get the list of available codecs
458 | final int numCodecs = MediaCodecList.getCodecCount();
459 | for (int i = 0; i < numCodecs; i++) {
460 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
461 | if (!codecInfo.isEncoder()) { // skipp decoder
462 | continue;
463 | }
464 | final String[] types = codecInfo.getSupportedTypes();
465 | for (String type : types) {
466 | if (type.contains("video")) {
467 | mVideoMap.put(codecInfo.getName(), type);
468 | }
469 | }
470 | }
471 | }
472 |
473 | // Get supported audio mime types
474 | HashMap mAudioMap= new HashMap<>();
475 | public HashMap getSupportedAudioMimeTypes(){
476 | checkIfSupportedAudioMimeTypes();
477 | return mAudioMap;
478 | }
479 | private void checkIfSupportedAudioMimeTypes(){
480 | // get the list of available codecs
481 | final int numCodecs = MediaCodecList.getCodecCount();
482 | for (int i = 0; i < numCodecs; i++) {
483 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
484 | if (!codecInfo.isEncoder()) { // skipp decoder
485 | continue;
486 | }
487 | final String[] types = codecInfo.getSupportedTypes();
488 | for (String type : types) {
489 | if (type.contains("audio")) {
490 | mAudioMap.put(codecInfo.getName(), type);
491 | }
492 | }
493 | }
494 | }
495 |
496 | }
497 |
--------------------------------------------------------------------------------
/hbrecorder/src/main/java/com/hbisoft/hbrecorder/HBRecorderListener.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import android.os.Build;
4 |
5 | import androidx.annotation.RequiresApi;
6 |
7 | public interface HBRecorderListener {
8 | void HBRecorderOnStart();
9 | void HBRecorderOnComplete();
10 | void HBRecorderOnError(int errorCode, String reason);
11 | @RequiresApi(api = Build.VERSION_CODES.N)
12 | void HBRecorderOnPause();
13 | @RequiresApi(api = Build.VERSION_CODES.N)
14 | void HBRecorderOnResume();
15 | }
16 |
--------------------------------------------------------------------------------
/hbrecorder/src/main/java/com/hbisoft/hbrecorder/MyListener.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | interface MyListener {
4 | void onCompleteCallback();
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/hbrecorder/src/main/java/com/hbisoft/hbrecorder/NotificationReceiver.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import android.content.BroadcastReceiver;
4 | import android.content.Context;
5 | import android.content.Intent;
6 |
7 | public class NotificationReceiver extends BroadcastReceiver {
8 |
9 | @Override
10 | public void onReceive(Context context, Intent intent) {
11 | Intent service = new Intent(context, ScreenRecordService.class);
12 | context.stopService(service);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/hbrecorder/src/main/java/com/hbisoft/hbrecorder/ScreenRecordService.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import android.app.Activity;
4 | import android.app.Notification;
5 | import android.app.NotificationChannel;
6 | import android.app.NotificationManager;
7 | import android.app.PendingIntent;
8 | import android.app.Service;
9 | import android.content.ContentResolver;
10 | import android.content.Context;
11 | import android.content.Intent;
12 | import android.content.pm.ServiceInfo;
13 | import android.graphics.Bitmap;
14 | import android.graphics.BitmapFactory;
15 | import android.graphics.Color;
16 | import android.graphics.drawable.Icon;
17 | import android.hardware.display.DisplayManager;
18 | import android.hardware.display.VirtualDisplay;
19 | import android.media.MediaRecorder;
20 | import android.media.projection.MediaProjection;
21 | import android.media.projection.MediaProjectionManager;
22 | import android.net.Uri;
23 | import android.os.Build;
24 | import android.os.Bundle;
25 | import android.os.Environment;
26 | import android.os.Handler;
27 | import android.os.IBinder;
28 |
29 | import androidx.annotation.RequiresApi;
30 |
31 | import android.os.Looper;
32 | import android.os.ResultReceiver;
33 | import android.util.Log;
34 |
35 | import java.io.FileDescriptor;
36 | import java.sql.Date;
37 | import java.text.SimpleDateFormat;
38 | import java.util.Locale;
39 | import java.util.Objects;
40 |
41 | import static com.hbisoft.hbrecorder.Constants.ERROR_KEY;
42 | import static com.hbisoft.hbrecorder.Constants.ERROR_REASON_KEY;
43 | import static com.hbisoft.hbrecorder.Constants.MAX_FILE_SIZE_REACHED_ERROR;
44 | import static com.hbisoft.hbrecorder.Constants.MAX_FILE_SIZE_KEY;
45 | import static com.hbisoft.hbrecorder.Constants.NO_SPECIFIED_MAX_SIZE;
46 | import static com.hbisoft.hbrecorder.Constants.ON_COMPLETE;
47 | import static com.hbisoft.hbrecorder.Constants.ON_COMPLETE_KEY;
48 | import static com.hbisoft.hbrecorder.Constants.ON_PAUSE;
49 | import static com.hbisoft.hbrecorder.Constants.ON_PAUSE_KEY;
50 | import static com.hbisoft.hbrecorder.Constants.ON_RESUME;
51 | import static com.hbisoft.hbrecorder.Constants.ON_RESUME_KEY;
52 | import static com.hbisoft.hbrecorder.Constants.ON_START;
53 | import static com.hbisoft.hbrecorder.Constants.ON_START_KEY;
54 | import static com.hbisoft.hbrecorder.Constants.SETTINGS_ERROR;
55 |
56 | /**
57 | * Created by HBiSoft on 13 Aug 2019
58 | * Copyright (c) 2019 . All rights reserved.
59 | */
60 |
61 | public class ScreenRecordService extends Service {
62 |
63 | private static final String TAG = "ScreenRecordService";
64 | private long maxFileSize = NO_SPECIFIED_MAX_SIZE;
65 | private boolean hasMaxFileBeenReached = false;
66 | private int mScreenWidth;
67 | private int mScreenHeight;
68 | private int mScreenDensity;
69 | private int mResultCode;
70 | private Intent mResultData;
71 | private boolean isVideoHD;
72 | private boolean isAudioEnabled;
73 | private String path;
74 |
75 | private String outputFormat;
76 |
77 | private MediaProjection mMediaProjection;
78 | private MediaRecorder mMediaRecorder;
79 | private VirtualDisplay mVirtualDisplay;
80 | private String name;
81 | private int audioBitrate;
82 | private int audioSamplingRate;
83 | private static String filePath;
84 | private static String fileName;
85 | private int audioSourceAsInt;
86 | private int videoEncoderAsInt;
87 | private boolean isCustomSettingsEnabled;
88 | private int videoFrameRate;
89 | private int videoBitrate;
90 | private int outputFormatAsInt;
91 | private int orientationHint;
92 |
93 | public final static String BUNDLED_LISTENER = "listener";
94 | private Uri returnedUri = null;
95 | private Intent mIntent;
96 |
97 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
98 | @Override
99 | public int onStartCommand(final Intent intent, int flags, int startId) {
100 | boolean isAction = false;
101 |
102 | //Check if there was an action called
103 | if (intent != null) {
104 |
105 | if (intent.getAction() != null) {
106 | isAction = true;
107 | }
108 |
109 | //If there was an action, check what action it was
110 | //Called when recording should be paused or resumed
111 | if (isAction) {
112 | //Pause Recording
113 | if (intent.getAction().equals("pause")) {
114 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
115 | pauseRecording();
116 | }
117 | }
118 | //Resume Recording
119 | else if (intent.getAction().equals("resume")) {
120 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
121 | resumeRecording();
122 | }
123 | }
124 | }
125 | //Start Recording
126 | else {
127 | //Get intent extras
128 | hasMaxFileBeenReached = false;
129 | mIntent = intent;
130 | maxFileSize = intent.getLongExtra(MAX_FILE_SIZE_KEY, NO_SPECIFIED_MAX_SIZE);
131 | byte[] notificationSmallIcon = intent.getByteArrayExtra("notificationSmallBitmap");
132 | int notificationSmallVector = intent.getIntExtra("notificationSmallVector", 0);
133 | String notificationTitle = intent.getStringExtra("notificationTitle");
134 | String notificationDescription = intent.getStringExtra("notificationDescription");
135 | String notificationButtonText = intent.getStringExtra("notificationButtonText");
136 | orientationHint = intent.getIntExtra("orientation", 400);
137 | mResultCode = intent.getIntExtra("code", -1);
138 | mResultData = intent.getParcelableExtra("data");
139 | mScreenWidth = intent.getIntExtra("width", 0);
140 | mScreenHeight = intent.getIntExtra("height", 0);
141 |
142 | if (intent.getStringExtra("mUri") != null) {
143 | returnedUri = Uri.parse(intent.getStringExtra("mUri"));
144 | }
145 |
146 | if (mScreenHeight == 0 || mScreenWidth == 0) {
147 | HBRecorderCodecInfo hbRecorderCodecInfo = new HBRecorderCodecInfo();
148 | hbRecorderCodecInfo.setContext(this);
149 | mScreenHeight = hbRecorderCodecInfo.getMaxSupportedHeight();
150 | mScreenWidth = hbRecorderCodecInfo.getMaxSupportedWidth();
151 | }
152 |
153 | mScreenDensity = intent.getIntExtra("density", 1);
154 | isVideoHD = intent.getBooleanExtra("quality", true);
155 | isAudioEnabled = intent.getBooleanExtra("audio", true);
156 | path = intent.getStringExtra("path");
157 | name = intent.getStringExtra("fileName");
158 | String audioSource = intent.getStringExtra("audioSource");
159 | String videoEncoder = intent.getStringExtra("videoEncoder");
160 | videoFrameRate = intent.getIntExtra("videoFrameRate", 30);
161 | videoBitrate = intent.getIntExtra("videoBitrate", 40000000);
162 |
163 | if (audioSource != null) {
164 | if (isAudioEnabled) {
165 | setAudioSourceAsInt(audioSource);
166 | }
167 | }
168 | if (videoEncoder != null) {
169 | setvideoEncoderAsInt(videoEncoder);
170 | }
171 |
172 | filePath = name;
173 | if (isAudioEnabled) {
174 | audioBitrate = intent.getIntExtra("audioBitrate", 128000);
175 | audioSamplingRate = intent.getIntExtra("audioSamplingRate", 44100);
176 | }
177 | outputFormat = intent.getStringExtra("outputFormat");
178 | if (outputFormat != null) {
179 | setOutputFormatAsInt(outputFormat);
180 | }
181 |
182 | isCustomSettingsEnabled = intent.getBooleanExtra("enableCustomSettings", false);
183 |
184 | //Set notification notification button text if developer did not
185 | if (notificationButtonText == null) {
186 | notificationButtonText = "STOP RECORDING";
187 | }
188 | //Set notification bitrate if developer did not
189 | if (audioBitrate == 0) {
190 | if (isAudioEnabled) {
191 | audioBitrate = 128000;
192 | }
193 | }
194 | //Set notification sampling rate if developer did not
195 | if (audioSamplingRate == 0) {
196 | if (isAudioEnabled) {
197 | audioSamplingRate = 44100;
198 | }
199 | }
200 | //Set notification title if developer did not
201 | if (notificationTitle == null || notificationTitle.equals("")) {
202 | notificationTitle = getString(R.string.stop_recording_notification_title);
203 | }
204 | //Set notification description if developer did not
205 | if (notificationDescription == null || notificationDescription.equals("")) {
206 | notificationDescription = getString(R.string.stop_recording_notification_message);
207 | }
208 |
209 | //Notification
210 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
211 | String channelId = "001";
212 | String channelName = "RecordChannel";
213 | NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE);
214 | channel.setLightColor(Color.BLUE);
215 | channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
216 | NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
217 | if (manager != null) {
218 | manager.createNotificationChannel(channel);
219 | Notification notification;
220 |
221 | Intent myIntent = new Intent(this, NotificationReceiver.class);
222 | PendingIntent pendingIntent;
223 |
224 | if (Build.VERSION.SDK_INT >= 31) {
225 | pendingIntent = PendingIntent.getBroadcast(this, 0, myIntent, PendingIntent.FLAG_IMMUTABLE);
226 | } else {
227 | pendingIntent = PendingIntent.getBroadcast(this, 0, myIntent, 0);
228 |
229 | }
230 |
231 | Notification.Action action = new Notification.Action.Builder(
232 | Icon.createWithResource(this, android.R.drawable.presence_video_online),
233 | notificationButtonText,
234 | pendingIntent).build();
235 |
236 | if (notificationSmallIcon != null) {
237 | Bitmap bmp = BitmapFactory.decodeByteArray(notificationSmallIcon, 0, notificationSmallIcon.length);
238 | //Modify notification badge
239 | notification = new Notification.Builder(getApplicationContext(), channelId).setOngoing(true).setSmallIcon(Icon.createWithBitmap(bmp)).setContentTitle(notificationTitle).setContentText(notificationDescription).addAction(action).build();
240 |
241 | } else if (notificationSmallVector != 0) {
242 | notification = new Notification.Builder(getApplicationContext(), channelId).setOngoing(true).setSmallIcon(notificationSmallVector).setContentTitle(notificationTitle).setContentText(notificationDescription).addAction(action).build();
243 | } else {
244 | //Modify notification badge
245 | notification = new Notification.Builder(getApplicationContext(), channelId).setOngoing(true).setSmallIcon(R.drawable.icon).setContentTitle(notificationTitle).setContentText(notificationDescription).addAction(action).build();
246 | }
247 | startFgs(101, notification);
248 | }
249 | } else {
250 | startFgs(101, new Notification());
251 | }
252 |
253 |
254 | if (returnedUri == null) {
255 | if (path == null) {
256 | path = String.valueOf(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES));
257 | }
258 | }
259 |
260 | //Init MediaRecorder
261 | try {
262 | initRecorder();
263 | } catch (Exception e) {
264 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
265 | Bundle bundle = new Bundle();
266 | bundle.putString(ERROR_REASON_KEY, Log.getStackTraceString(e));
267 | if (receiver != null) {
268 | receiver.send(Activity.RESULT_OK, bundle);
269 | }
270 | }
271 |
272 | //Init MediaProjection
273 | try {
274 | initMediaProjection();
275 | } catch (Exception e) {
276 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
277 | Bundle bundle = new Bundle();
278 | bundle.putString(ERROR_REASON_KEY, Log.getStackTraceString(e));
279 | if (receiver != null) {
280 | receiver.send(Activity.RESULT_OK, bundle);
281 | }
282 | }
283 |
284 | //Init VirtualDisplay
285 | try {
286 | initVirtualDisplay();
287 | } catch (Exception e) {
288 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
289 | Bundle bundle = new Bundle();
290 | bundle.putString(ERROR_REASON_KEY, Log.getStackTraceString(e));
291 | if (receiver != null) {
292 | receiver.send(Activity.RESULT_OK, bundle);
293 | }
294 | }
295 |
296 | mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() {
297 | @Override
298 | public void onError(MediaRecorder mediaRecorder, int what, int extra) {
299 | if (what == 268435556 && hasMaxFileBeenReached) {
300 | // Benign error b/c recording is too short and has no frames. See SO: https://stackoverflow.com/questions/40616466/mediarecorder-stop-failed-1007
301 | return;
302 | }
303 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
304 | Bundle bundle = new Bundle();
305 | bundle.putInt(ERROR_KEY, SETTINGS_ERROR);
306 | bundle.putString(ERROR_REASON_KEY, String.valueOf(what));
307 | if (receiver != null) {
308 | receiver.send(Activity.RESULT_OK, bundle);
309 | }
310 | }
311 | });
312 |
313 | mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() {
314 | @Override
315 | public void onInfo(MediaRecorder mr, int what, int extra) {
316 | if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
317 | hasMaxFileBeenReached = true;
318 | Log.i(TAG, String.format(Locale.US, "onInfoListen what : %d | extra %d", what, extra));
319 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
320 | Bundle bundle = new Bundle();
321 | bundle.putInt(ERROR_KEY, MAX_FILE_SIZE_REACHED_ERROR);
322 | bundle.putString(ERROR_REASON_KEY, getString(R.string.max_file_reached));
323 | if (receiver != null) {
324 | receiver.send(Activity.RESULT_OK, bundle);
325 | }
326 | }
327 | }
328 | });
329 |
330 | //Start Recording
331 | try {
332 | mMediaRecorder.start();
333 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
334 | Bundle bundle = new Bundle();
335 | bundle.putInt(ON_START_KEY, ON_START);
336 | if (receiver != null) {
337 | receiver.send(Activity.RESULT_OK, bundle);
338 | }
339 | } catch (Exception e) {
340 | // From the tests I've done, this can happen if another application is using the mic or if an unsupported video encoder was selected
341 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
342 | Bundle bundle = new Bundle();
343 | bundle.putInt(ERROR_KEY, SETTINGS_ERROR);
344 | bundle.putString(ERROR_REASON_KEY, Log.getStackTraceString(e));
345 | if (receiver != null) {
346 | receiver.send(Activity.RESULT_OK, bundle);
347 | }
348 | }
349 | }
350 | } else {
351 | stopSelf(startId);
352 | }
353 |
354 | return Service.START_STICKY;
355 | }
356 |
357 | //Pause Recording
358 | @RequiresApi(api = Build.VERSION_CODES.N)
359 | private void pauseRecording(){
360 | mMediaRecorder.pause();
361 | ResultReceiver receiver = mIntent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
362 | Bundle bundle = new Bundle();
363 | bundle.putString(ON_PAUSE_KEY, ON_PAUSE);
364 | if (receiver != null) {
365 | receiver.send(Activity.RESULT_OK, bundle);
366 | }
367 | }
368 |
369 | //Resume Recording
370 | @RequiresApi(api = Build.VERSION_CODES.N)
371 | private void resumeRecording(){
372 | mMediaRecorder.resume();
373 | ResultReceiver receiver = mIntent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
374 | Bundle bundle = new Bundle();
375 | bundle.putString(ON_RESUME_KEY, ON_RESUME);
376 | if (receiver != null) {
377 | receiver.send(Activity.RESULT_OK, bundle);
378 | }
379 | }
380 |
381 | //Set output format as int based on what developer has provided
382 | //It is important to provide one of the following and nothing else.
383 | private void setOutputFormatAsInt(String outputFormat) {
384 | switch (outputFormat) {
385 | case "DEFAULT":
386 | outputFormatAsInt = 0;
387 | break;
388 | case "THREE_GPP":
389 | outputFormatAsInt = 1;
390 | break;
391 | case "AMR_NB":
392 | outputFormatAsInt = 3;
393 | break;
394 | case "AMR_WB":
395 | outputFormatAsInt = 4;
396 | break;
397 | case "AAC_ADTS":
398 | outputFormatAsInt = 6;
399 | break;
400 | case "MPEG_2_TS":
401 | outputFormatAsInt = 8;
402 | break;
403 | case "WEBM":
404 | outputFormatAsInt = 9;
405 | break;
406 | case "OGG":
407 | outputFormatAsInt = 11;
408 | break;
409 | case "MPEG_4":
410 | default:
411 | outputFormatAsInt = 2;
412 | }
413 | }
414 |
415 | private String getExtension(String outputFormat) {
416 | switch (outputFormat) {
417 | case "THREE_GPP":
418 | return ".3gp";
419 | case "AMR_NB":
420 | return ".amr";
421 | case "AMR_WB":
422 | return ".amr";
423 | case "AAC_ADTS":
424 | return ".aac";
425 | case "MPEG_2_TS":
426 | return ".ts";
427 | case "WEBM":
428 | return ".webm";
429 | case "OGG":
430 | return ".ogg";
431 | default:
432 | return ".mp4"; // Default to .mp4 for unknown formats
433 | }
434 | }
435 |
436 | //Set video encoder as int based on what developer has provided
437 | //It is important to provide one of the following and nothing else.
438 | private void setvideoEncoderAsInt(String encoder) {
439 | switch (encoder) {
440 | case "DEFAULT":
441 | videoEncoderAsInt = 0;
442 | break;
443 | case "H263":
444 | videoEncoderAsInt = 1;
445 | break;
446 | case "H264":
447 | videoEncoderAsInt = 2;
448 | break;
449 | case "MPEG_4_SP":
450 | videoEncoderAsInt = 3;
451 | break;
452 | case "VP8":
453 | videoEncoderAsInt = 4;
454 | break;
455 | case "HEVC":
456 | videoEncoderAsInt = 5;
457 | break;
458 | }
459 | }
460 |
461 | //Set audio source as int based on what developer has provided
462 | //It is important to provide one of the following and nothing else.
463 | private void setAudioSourceAsInt(String audioSource) {
464 | switch (audioSource) {
465 | case "DEFAULT":
466 | audioSourceAsInt = 0;
467 | break;
468 | case "MIC":
469 | audioSourceAsInt = 1;
470 | break;
471 | case "VOICE_UPLINK":
472 | audioSourceAsInt = 2;
473 | break;
474 | case "VOICE_DOWNLINK":
475 | audioSourceAsInt = 3;
476 | break;
477 | case "VOICE_CALL":
478 | audioSourceAsInt = 4;
479 | break;
480 | case "CAMCODER":
481 | audioSourceAsInt = 5;
482 | break;
483 | case "VOICE_RECOGNITION":
484 | audioSourceAsInt = 6;
485 | break;
486 | case "VOICE_COMMUNICATION":
487 | audioSourceAsInt = 7;
488 | break;
489 | case "REMOTE_SUBMIX":
490 | audioSourceAsInt = 8;
491 | break;
492 | case "UNPROCESSED":
493 | audioSourceAsInt = 9;
494 | break;
495 | case "VOICE_PERFORMANCE":
496 | audioSourceAsInt = 10;
497 | break;
498 | }
499 | }
500 |
501 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
502 | private void initMediaProjection() {
503 | mMediaProjection = ((MediaProjectionManager) Objects.requireNonNull(getSystemService(Context.MEDIA_PROJECTION_SERVICE))).getMediaProjection(mResultCode, mResultData);
504 | Handler handler = new Handler(Looper.getMainLooper());
505 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
506 | mMediaProjection.registerCallback(new MediaProjection.Callback() {
507 | @Override
508 | public void onStop() {
509 | super.onStop();
510 | }
511 | }, handler);
512 | } else {
513 | mMediaProjection.registerCallback(new MediaProjection.Callback() {
514 | // Nothing
515 | // We don't use it but register it to avoid runtime error from SDK 34+.
516 | }, handler);
517 | }
518 | }
519 |
520 | //Return the output file path as string
521 | public static String getFilePath() {
522 | return filePath;
523 | }
524 |
525 | //Return the name of the output file
526 | public static String getFileName() {
527 | return fileName;
528 | }
529 |
530 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
531 | private void initRecorder() throws Exception {
532 | SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault());
533 | Date curDate = new Date(System.currentTimeMillis());
534 | String curTime = formatter.format(curDate).replace(" ", "");
535 | String videoQuality = "HD";
536 | if (!isVideoHD) {
537 | videoQuality = "SD";
538 | }
539 | if (name == null) {
540 | name = videoQuality + curTime;
541 | }
542 |
543 | filePath = path + "/" + name + ".mp4";
544 |
545 | fileName = name + ".mp4";
546 |
547 | mMediaRecorder = new MediaRecorder();
548 |
549 |
550 | if (isAudioEnabled) {
551 | mMediaRecorder.setAudioSource(audioSourceAsInt);
552 | }
553 | mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
554 | mMediaRecorder.setOutputFormat(outputFormatAsInt);
555 |
556 | if (orientationHint != 400){
557 | mMediaRecorder.setOrientationHint(orientationHint);
558 | }
559 |
560 | if (isAudioEnabled) {
561 | mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
562 | mMediaRecorder.setAudioEncodingBitRate(audioBitrate);
563 | mMediaRecorder.setAudioSamplingRate(audioSamplingRate);
564 | }
565 |
566 | mMediaRecorder.setVideoEncoder(videoEncoderAsInt);
567 |
568 |
569 | if (returnedUri != null) {
570 | try {
571 | ContentResolver contentResolver = getContentResolver();
572 | FileDescriptor inputPFD = Objects.requireNonNull(contentResolver.openFileDescriptor(returnedUri, "rw")).getFileDescriptor();
573 | mMediaRecorder.setOutputFile(inputPFD);
574 | } catch (Exception e) {
575 | ResultReceiver receiver = mIntent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
576 | Bundle bundle = new Bundle();
577 | bundle.putString(ERROR_REASON_KEY, Log.getStackTraceString(e));
578 | if (receiver != null) {
579 | receiver.send(Activity.RESULT_OK, bundle);
580 | }
581 | }
582 | }else{
583 | if (outputFormat!=null){
584 | filePath = path + "/" + name + getExtension(outputFormat);
585 | fileName = name + getExtension(outputFormat);
586 | }else {
587 | filePath = path + "/" + name + ".mp4";
588 | fileName = name + ".mp4";
589 | }
590 | mMediaRecorder.setOutputFile(filePath);
591 | }
592 | mMediaRecorder.setVideoSize(mScreenWidth, mScreenHeight);
593 |
594 | if (!isCustomSettingsEnabled) {
595 | if (!isVideoHD) {
596 | mMediaRecorder.setVideoEncodingBitRate(12000000);
597 | mMediaRecorder.setVideoFrameRate(30);
598 | } else {
599 | mMediaRecorder.setVideoEncodingBitRate(5 * mScreenWidth * mScreenHeight);
600 | mMediaRecorder.setVideoFrameRate(60); //after setVideoSource(), setOutFormat()
601 | }
602 | } else {
603 | mMediaRecorder.setVideoEncodingBitRate(videoBitrate);
604 | mMediaRecorder.setVideoFrameRate(videoFrameRate);
605 | }
606 |
607 | // Catch approaching file limit
608 | if ( maxFileSize > NO_SPECIFIED_MAX_SIZE) {
609 | mMediaRecorder.setMaxFileSize(maxFileSize); // in bytes
610 | }
611 |
612 | mMediaRecorder.prepare();
613 |
614 | }
615 |
616 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
617 | private void initVirtualDisplay() {
618 | if (mMediaProjection == null) {
619 | Log.d(TAG, "initVirtualDisplay: " + " Media projection is not initialized properly.");
620 | return;
621 | }
622 | mVirtualDisplay = mMediaProjection.createVirtualDisplay(TAG, mScreenWidth, mScreenHeight, mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder.getSurface(), null, null);
623 | }
624 |
625 | private void startFgs(int notificationId, Notification notificaton) {
626 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE){
627 | if (isAudioEnabled){
628 | startForeground(notificationId, notificaton, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
629 | }else {
630 | startForeground(notificationId, notificaton, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
631 | }
632 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
633 | startForeground(notificationId, notificaton, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
634 | } else {
635 | startForeground(notificationId, notificaton);
636 | }
637 | }
638 |
639 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
640 | @Override
641 | public void onDestroy() {
642 | super.onDestroy();
643 | resetAll();
644 | callOnComplete();
645 |
646 | }
647 |
648 | private void callOnComplete() {
649 | if ( mIntent != null ) {
650 | ResultReceiver receiver = mIntent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
651 | Bundle bundle = new Bundle();
652 | bundle.putString(ON_COMPLETE_KEY, ON_COMPLETE);
653 | if (receiver != null) {
654 | receiver.send(Activity.RESULT_OK, bundle);
655 | }
656 | }
657 | }
658 |
659 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
660 | private void resetAll() {
661 | stopForeground(true);
662 | if (mVirtualDisplay != null) {
663 | mVirtualDisplay.release();
664 | mVirtualDisplay = null;
665 | }
666 | if (mMediaRecorder != null) {
667 | mMediaRecorder.setOnErrorListener(null);
668 | mMediaRecorder.reset();
669 | }
670 | if (mMediaProjection != null) {
671 | mMediaProjection.stop();
672 | mMediaProjection = null;
673 | }
674 | }
675 |
676 | @Override
677 | public IBinder onBind(Intent intent) {
678 | return null;
679 | }
680 | }
681 |
--------------------------------------------------------------------------------
/hbrecorder/src/main/res/drawable/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HBiSoft/HBRecorder/b8c941de636b77376b818bebc61fd87ac4c03975/hbrecorder/src/main/res/drawable/icon.png
--------------------------------------------------------------------------------
/hbrecorder/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | HBRecorder
3 | File size max has been reached.
4 | Drag down to stop the recording
5 | Recording your screen
6 |
7 |
--------------------------------------------------------------------------------
/hbrecorder/src/test/java/com/hbisoft/hbrecorder/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk17
3 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':hbrecorder'
2 |
--------------------------------------------------------------------------------