diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/build.gradle b/dConnectDevicePlugin/dConnectDeviceUVC/app/build.gradle index 89375539d9..646eae2b8e 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/build.gradle +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' + // Create github.properties in root project folder file with // gpr.usr=GITHUB_USER_ID // gpr.key=PERSONAL_ACCESS_TOKEN @@ -7,8 +8,9 @@ def githubProperties = new Properties() if (githubPropertiesFile.exists()) { githubProperties.load(new FileInputStream(githubPropertiesFile)) } + android { - compileSdkVersion 29 + compileSdkVersion 30 def getVersionName = { -> def version @@ -46,6 +48,10 @@ android { } } + dataBinding { + enabled = true + } + packagingOptions { exclude 'LICENSE.txt' exclude 'META-INF/DEPENDENCIES' @@ -61,6 +67,7 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + repositories { maven { name = "DeviceConnect-Android" @@ -76,8 +83,19 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation 'com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:1.9.1' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation "androidx.navigation:navigation-fragment:2.3.3" + implementation "androidx.navigation:navigation-ui:2.3.3" + implementation "androidx.navigation:navigation-fragment-ktx:2.3.3" + implementation "androidx.navigation:navigation-ui-ktx:2.3.3" + implementation "androidx.preference:preference:1.1.1" + implementation 'androidx.lifecycle:lifecycle-viewmodel:2.3.0' + implementation 'androidx.lifecycle:lifecycle-livedata:2.3.0' + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + implementation 'com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:1.9.7' implementation 'org.deviceconnect:dconnect-device-plugin-sdk:2.8.6' - implementation 'org.deviceconnect:libmedia:1.1.2' - implementation project(':libuvccamera') + implementation 'org.deviceconnect:libmedia:1.2.2' + implementation 'org.deviceconnect:libsrt:1.2.2' + implementation project(':libuvc') } diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/AndroidManifest.xml b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/AndroidManifest.xml index 38a39cf1cb..289e4e954d 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/AndroidManifest.xml +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/AndroidManifest.xml @@ -1,34 +1,43 @@ - - + package="org.deviceconnect.android.deviceplugin.uvc"> + + + - - - + android:label="@string/app_name_uvc" + android:theme="@style/AppTheme"> - - + + - - + + + + - + + + @@ -39,32 +48,9 @@ - - + + - - - - - - - - - - - - diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/assets/org_deviceconnect_android_deviceplugin_uvc/api/camera.json b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/assets/org_deviceconnect_android_deviceplugin_uvc/api/camera.json new file mode 100644 index 0000000000..b356d2eb1d --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/assets/org_deviceconnect_android_deviceplugin_uvc/api/camera.json @@ -0,0 +1,333 @@ + +{ + "swagger": "2.0", + "info": { + "description": "スマートデバイスのカメラ操作機能を提供する API。", + "version": "2.0.0", + "title": "Camera Profile" + }, + "basePath": "/gotapi/camera", + "consumes": ["application/x-www-form-urlencoded", "multipart/form-data"], + "paths": { + "/options": { + "get": { + "summary": "カメラの静止画・動画の撮影設定を取得します。", + "description": "カメラの静止画・動画の撮影設定を取得します。", + "operationId": "cameraOptionsGet", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "query", + "description": "カメラを識別するID。
省略された場合にはデフォルトに設定されているカメラに設定を行います。
MediaStreamRecording と共通の ID になります。", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "カメラの静止画・動画の撮影設定を返す。
設定の取得に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/GetOptionResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" + }, + "put": { + "summary": "カメラの静止画・動画の撮影設定を行います。", + "description": "カメラの静止画・動画の撮影設定を行います。", + "operationId": "cameraOptionsPut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "カメラを識別するID。
省略された場合にはデフォルトに設定されているカメラに設定を行います。", + "required": false, + "type": "string" + }, + { + "name": "autoFocus", + "in": "formData", + "description": "オートフォーカスのモードを設定します。", + "required": false, + "type": "string", + "enum": ["none", "off", "auto", "macro", "continuous_video", "continuous_picture", "edof"] + }, + { + "name": "whiteBalance", + "in": "formData", + "description": "ホワイトバランスのモードを設定します。", + "required": false, + "type": "string", + "enum": ["none", "off", "auto", "incandescent", "fluorescent", "warm-fluorescent", "daylight", "cloudy-daylight", "twilight", "shade"] + }, + { + "name": "whiteBalanceTemperature", + "in": "formData", + "description": "ホワイトバランスの色温度を設定します。", + "required": false, + "type": "integer" + }, + { + "name": "autoExposure", + "in": "formData", + "description": "露出モードを設定します。", + "required": false, + "type": "string", + "enum": ["none", "off", "on", "on_auto_flash", "on_always_flash", "on_auto_flash_redeye", "on_external_flash"] + }, + { + "name": "sensorExposureTime", + "in": "formData", + "description": "露出時間を設定します。", + "required": false, + "type": "integer" + }, + { + "name": "sensorSensitivity", + "in": "formData", + "description": "ISO感度を設定します。", + "required": false, + "type": "integer" + }, + { + "name": "sensorFrameDuration", + "in": "formData", + "description": "フレーム期間を設定します。", + "required": false, + "type": "integer" + }, + { + "name": "stabilization", + "in": "formData", + "description": "手ぶれ補正を設定します。", + "required": false, + "type": "string", + "enum": ["none", "off", "on"] + }, + { + "name": "opticalStabilization", + "in": "formData", + "description": "光学手ぶれ補正を設定します。", + "required": false, + "type": "string", + "enum": ["none", "off", "on"] + }, + { + "name": "noiseReduction", + "in": "formData", + "description": "ノイズ低減モードを設定します。", + "required": false, + "type": "string", + "enum": ["none", "off", "fast", "high_quality", "minimal", "zero_shutter_lag"] + }, + { + "name": "focalLength", + "in": "formData", + "description": "焦点距離を設定します。", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "カメラの静止画・動画の撮影設定結果を返す。
設定に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/PutOptionResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" + } + }, + "/zoom": { + "get": { + "summary": "Cameraデバイスの現在の倍率を取得する。", + "description": "倍率の値は0.0〜1.0で表現する。", + "operationId": "cameraZoomGet", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Cameraの現在の倍率を返す。 取得に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/CameraGetZoomResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0", + "zoomPosition": 0 + } + } + } + }, + "x-type": "one-shot" + }, + "put": { + "summary": "Cameraデバイスに対してレンズのズームイン・アウトリクエストを送る。", + "description": "ズームイン・アウトの倍率は、なるべく標準化し、どのデバイスでも同じように動くようにすること。", + "operationId": "cameraZoomPut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "direction", + "in": "formData", + "description": "方向。ズームの方向
in
out", + "required": true, + "type": "string", + "enum": ["in", "out"] + }, + { + "name": "movement", + "in": "formData", + "description": "動作。ズームの動作
in-startで開始してin-stopで終了。
1shotは開始してから所定位置まで動作する。
maxはin-startと同じ動作を行う。", + "required": true, + "type": "string", + "enum": ["in-start", "in-stop", "1shot", "max"] + } + ], + "responses": { + "200": { + "description": "ズームイン・アウトのリクエスト結果を返す。
失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/CameraSetZoomResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" + } + } + }, + "definitions": { + "GetOptionResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "properties": { + "exposureMode": { + "type": "string", + "description": "現在設定されている露出プログラムを取得します。", + "title": "露出プログラム" + }, + "shutterSpeed": { + "type": "string", + "description": "現在設定されているシャッター速度を取得します。", + "title": "シャッター速度" + } + } + } + ] + }, + "PutOptionResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + } + ] + }, + "CameraGetZoomResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "required": ["zoomPosition"], + "properties": { + "zoomPosition": { + "type": "number", + "description": "カメラの倍率(%)
例)
QX-10の場合
0(%):拡大なし
50(%):倍率5倍での拡大", + "title": "カメラの倍率" + } + } + } + ] + }, + "CameraSetZoomResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + } + ] + }, + "CommonResponse": { + "type": "object", + "required": ["product", "result", "version"], + "properties": { + "result": { + "type": "integer", + "description": "0: 正常応答
0以外: 異常応答", + "title": "処理結果" + }, + "product": { + "type": "string", + "description": "DeviceConnectシステムの名前。", + "title": "システム名" + }, + "version": { + "type": "string", + "description": "DeviceConnectシステムのバージョン名。", + "title": "システムバージョン" + }, + "hmac": { + "type": "string", + "description": "レスポンスに対する署名。 アプリケーション側から事前にHMACキーを共有されていた場合は必須。", + "title": "署名" + } + } + } + } +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/assets/org_deviceconnect_android_deviceplugin_uvc/api/mediaStreamRecording.json b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/assets/org_deviceconnect_android_deviceplugin_uvc/api/mediaStreamRecording.json new file mode 100644 index 0000000000..3dd8a8b02a --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/assets/org_deviceconnect_android_deviceplugin_uvc/api/mediaStreamRecording.json @@ -0,0 +1,1903 @@ +{ + "swagger": "2.0", + "basePath": "/gotapi/mediaStreamRecording", + "info": { + "title": "Media Stream Recording Profile", + "version": "2.0.0", + "description": "スマートデバイスによる写真撮影、動画録画または音声録音などの操作を行うAPI。" + }, + "consumes": [ + "application/x-www-form-urlencoded", + "multipart/form-data" + ], + "paths": { + "/mediaRecorder": { + "get": { + "operationId": "mediaStreamRecordingMediaRecorderGet", + "x-type": "one-shot", + "summary": "スマートデバイスから使用可能なレコーダーの一覧を取得する。", + "description": "ストリーミング配信する機能をレコーダーとして扱うことができる。 例えば、スマートフォンのカメラの映像や スマートフォンのデスクトップのスクリーンキャストなどをレコーダとして扱ったりすることができる。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "query", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "レコーダの一覧を取得結果を返す。 取得に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/MediaRecorderResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0", + "recorders": [ + { + "id": "photo_0", + "name": "Example Camera Recorder - back", + "state": "inactive", + "imageWidth": 3264, + "imageHeight": 2448, + "previewWidth": 640, + "previewHeight": 480, + "previewMaxFrameRate": 10, + "mimeType": "image/png", + "config": "" + }, + { + "id": "photo_1", + "name": "Example Camera Recorder - front", + "state": "inactive", + "imageWidth": 1280, + "imageHeight": 960, + "previewWidth": 640, + "previewHeight": 480, + "previewMaxFrameRate": 10, + "mimeType": "image/png", + "config": "" + }, + { + "id": "video_0", + "name": "Example Video Recorder - back", + "state": "inactive", + "imageWidth": 640, + "imageHeight": 480, + "mimeType": "video/3gp", + "config": "" + }, + { + "id": "video_1", + "name": "Example Video Recorder - front", + "state": "inactive", + "imageWidth": 640, + "imageHeight": 480, + "mimeType": "video/3gp", + "config": "" + }, + { + "id": "audio", + "name": "Example Audio Recorder", + "state": "inactive", + "mimeType": "audio/3gp", + "config": "" + }, + { + "id": "screen", + "name": "Example Screen", + "state": "inactive", + "imageWidth": 1080, + "imageHeight": 1776, + "previewWidth": 270, + "previewHeight": 444, + "previewMaxFrameRate": 10, + "mimeType": "video/x-mjpeg", + "config": "" + } + ] + } + } + } + } + } + }, + "/takePhoto": { + "post": { + "operationId": "mediaStreamRecordingTakePhotoPost", + "x-type": "one-shot", + "summary": "スマートデバイスに対して写真撮影リクエストを送る。", + "description": "targetが指定されていない場合は、GET/mediaStreamRecording/mediaRecorderで 一番最初に見つかるレコーダーが指定される。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "写真撮影結果を返す。 写真撮影に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/TakePhotoResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0", + "uri": "Example URI", + "path": "Example Path" + } + } + } + } + } + }, + "/record": { + "post": { + "operationId": "mediaStreamRecordingRecordPost", + "x-type": "one-shot", + "summary": "スマートデバイスに対して、動画撮影や音声録音の開始リクエストを送る。", + "description": "MediaStreamRecording Stopされない場合は各デバイスが撮影できる最大時間まで 撮影を行い、 その後撮影を停止する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + }, + { + "name": "timeslice", + "description": "タイムスライス。
動画・音声入力などから得られた1フレームを新たに出力先メディアに 書き出すまでの待ち時間。単位はミリ秒。
1000/timesliceが出力メディアの固定フレームレート(フレーム/秒) に相当する。省略された場合には、デバイス毎の挙動でフレーム書き出しを行う。", + "in": "formData", + "required": false, + "minimum": 0, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "動画撮影・音声録音開始結果を返す。 開始に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecordResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0", + "uri": "Example URI", + "path": "Example Path" + } + } + } + } + } + }, + "/pause": { + "put": { + "operationId": "mediaStreamRecordingPausePut", + "x-type": "one-shot", + "summary": "スマートデバイスに対して動画撮影または音声録音の一時停止リクエストを送る。", + "description": "すでに撮影または録音が一時停止になっている場合や撮影または録音が行われていない場合は、 エラーを返す。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "動画撮影または音声録音の一時停止送信結果を返す。 一時停止送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + }, + "/resume": { + "put": { + "operationId": "mediaStreamRecordingResumePut", + "x-type": "one-shot", + "summary": "スマートデバイスに対して一時停止状態にある動画撮影または音声録音の 再開リクエストを送る。", + "description": "すでに撮影または録音状態になっている場合はエラーを返す。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "再開リクエストを送った結果を返す。 リクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + }, + "/stop": { + "put": { + "operationId": "mediaStreamRecordingStopPut", + "x-type": "one-shot", + "summary": "スマートデバイスに対して動画撮影または音声録音の終了リクエストを送る。", + "description": "すでに撮影または録音が行われていない場合はエラーを返す。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "終了リクエストを送った結果を返す。 終了リクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderStopResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + }, + "/muteTrack": { + "put": { + "operationId": "mediaStreamRecordingMuteTrackPut", + "x-type": "one-shot", + "summary": "スマートデバイスに対して動画撮影や音声録音のミュートリクエストを送る。", + "description": "ビデオとオーディオなどの複数トラックが含まれる撮影中メディアにおいては、 オーディオトラックがミュートされる。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "ミュートリクエストを送った結果を返す。 ミュートリクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + }, + "/unmuteTrack": { + "put": { + "operationId": "mediaStreamRecordingUnmuteTrackPut", + "x-type": "one-shot", + "summary": "スマートデバイスに対して動画撮影や音声録音のミュート解除リクエストを送る。", + "description": "ビデオとオーディオなどの複数トラックが含まれる撮影中メディアにおいては、 オーディオトラックがミュート解除される。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "ミュート解除リクエストを送った結果を返す。 ミュート解除リクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + }, + "/options": { + "get": { + "operationId": "mediaStreamRecordingOptionsGet", + "x-type": "one-shot", + "summary": "スマートデバイスからサポートしている写真撮影、 動画撮影や音声録音のオプションを取得する。", + "description": "ターゲットがサポートしている解像度などの一覧を返す。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "query", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "query", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "指定したターゲットのPreview/Picture解像度の一覧などを返す。", + "schema": { + "$ref": "#/definitions/OptionsResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0", + "imageSize": [ + { + "width": 3264, + "height": 2448 + }, + { + "width": 3200, + "height": 2400 + }, + { + "width": 2592, + "height": 1944 + }, + { + "width": 2048, + "height": 1536 + }, + { + "width": 1920, + "height": 1080 + }, + { + "width": 1600, + "height": 1200 + }, + { + "width": 1280, + "height": 960 + }, + { + "width": 1280, + "height": 768 + }, + { + "width": 1280, + "height": 720 + }, + { + "width": 1024, + "height": 768 + }, + { + "width": 800, + "height": 600 + }, + { + "width": 800, + "height": 480 + }, + { + "width": 720, + "height": 480 + }, + { + "width": 640, + "height": 480 + }, + { + "width": 352, + "height": 288 + }, + { + "width": 320, + "height": 240 + }, + { + "width": 176, + "height": 144 + } + ], + "previewSize": [ + { + "width": 1920, + "height": 1080 + }, + { + "width": 1600, + "height": 1200 + }, + { + "width": 1280, + "height": 960 + }, + { + "width": 1280, + "height": 768 + }, + { + "width": 1280, + "height": 720 + }, + { + "width": 1024, + "height": 768 + }, + { + "width": 800, + "height": 600 + }, + { + "width": 800, + "height": 480 + }, + { + "width": 720, + "height": 480 + }, + { + "width": 640, + "height": 480 + }, + { + "width": 352, + "height": 288 + }, + { + "width": 320, + "height": 240 + }, + { + "width": 176, + "height": 144 + } + ], + "mimeType": [ + "image/png" + ] + } + } + } + } + }, + "put": { + "operationId": "mediaStreamRecordingOptionsPut", + "x-type": "one-shot", + "summary": "スマートデバイスからサポートしている写真撮影、 動画撮影や音声録音のオプションを設定する。", + "description": "スマートデバイスのPreviewSize,PictureSizeや最大フレームレートなどを設定する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + }, + { + "name": "imageWidth", + "description": "撮影時の横幅。単位はピクセル。previewHeightを指定するときは省略不可。Options API GETで返された値以外を指定した場合はパラメータエラー。", + "in": "formData", + "required": false, + "type": "integer" + }, + { + "name": "imageHeight", + "description": "撮影時の縦幅。単位はピクセル。previewWidthを指定するときは省略不可。
Options API GETで返された値以外を指定した場合はパラメータエラー。", + "in": "formData", + "required": false, + "type": "integer" + }, + { + "name": "previewWidth", + "description": "プレビュー時の横幅。単位はピクセル。previewHeightを指定するときは省略不可。
Options API GETで返された値以外を指定した場合はパラメータエラー。", + "in": "formData", + "required": false, + "type": "integer" + }, + { + "name": "previewHeight", + "description": "プレビュー時の縦幅。単位はピクセル。previewWidthを指定するときは省略不可。
Options API GETで返された値以外を指定した場合はパラメータエラー。", + "in": "formData", + "required": false, + "type": "integer" + }, + { + "name": "previewMaxFrameRate", + "description": "プレビュー時の最大フレームレート。単位はfps。範囲は0.0より大きい小数値。", + "in": "formData", + "required": false, + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + { + "name": "previewBitRate", + "description": "プレビュー時のビットレート設定。単位はbps。範囲は0より大きい整数値。", + "in": "formData", + "required": false, + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + }, + { + "name": "previewKeyFrameInterval", + "description": "プレビューのキーフレーム送信間隔。単位は秒。", + "in": "formData", + "required": false, + "type": "integer" + }, + { + "name": "previewEncoder", + "description": "プレビュー配信を行う時のエンコーダ。
h264 とサポートしている場合には、h265を指定できる。", + "in": "formData", + "required": false, + "enum": [ + "h264", "h265" + ], + "type": "string" + }, + { + "name": "previewProfile", + "description": "エンコーダーのプロファイル名。", + "in": "formData", + "required": false, + "type": "string" + }, + { + "name": "previewLevel", + "description": "エンコーダーのレベル名。", + "in": "formData", + "required": false, + "type": "string" + }, + { + "name": "previewIntraRefresh", + "description": "イントラリフレッシュの値。
0 が指定された場合にはイントラリフレッシュを使用しない。", + "in": "formData", + "required": false, + "type": "integer" + }, + { + "name": "previewJpegQuality", + "description": "Motion JPEG を配信する時のクオリティ。
0.0 から 1.0 の範囲で設定できる。", + "in": "formData", + "required": false, + "minimum": 0.0, + "exclusiveMinimum": false, + "maximum": 1.0, + "exclusiveMaximum": false, + "type": "number" + }, + { + "name": "previewClipLeft", + "description": "プレビューの切り抜き範囲のx座標の開始位置。", + "in": "formData", + "required": false, + "type": "integer" + }, + { + "name": "previewClipTop", + "description": "プレビューの切り抜き範囲のy座標の開始位置。", + "in": "formData", + "required": false, + "type": "integer" + }, + { + "name": "previewClipRight", + "description": "プレビューの切り抜き範囲のx座標の終了位置。", + "in": "formData", + "required": false, + "type": "integer" + }, + { + "name": "previewClipBottom", + "description": "プレビューの切り抜き範囲のy座標の終了位置。", + "in": "formData", + "required": false, + "type": "integer" + }, + { + "name": "previewClipReset", + "description": "true が指定された時にプレビューの切り抜き範囲を解除する。
previewClipLeft などと同時に指定された場合には、previewClipReset を優先する。", + "in": "formData", + "required": false, + "type": "boolean" + }, + { + "name": "mimeType", + "description": "MimeType。動画録画・音声録音するときのエンコードするタイプ。", + "in": "formData", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "オプションの設定結果を返す。 設定に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + }, + "/onPhoto": { + "get": { + "operationId": "mediaStreamRecordingOnPhotoGet", + "x-type": "one-shot", + "summary": "スマートデバイスの写真撮影通知イベントを取得する。", + "description": "プラグイン側でキャッシュしている最新のイベントメッセージを1つ取得する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "query", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "写真撮影された情報を返す。 情報が返せない場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/PhotoResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0", + "uri": "Example URI", + "path": "Example Path" + } + } + } + } + }, + "put": { + "operationId": "mediaStreamRecordingOnPhotoPut", + "x-type": "event", + "summary": "スマートデバイスの写真撮影通知イベントを開始する。", + "description": "スマートデバイスでサポートしていないパラメータがある場合には、 そのパラメータを省略する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "interval", + "description": "デバイスプラグインがイベントを送信する間隔。", + "in": "formData", + "required": false, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "通知イベントの開始結果を返す。 開始に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/EventRegistrationResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-event": { + "schema": { + "$ref": "#/definitions/PhotoEvent" + }, + "examples": { + "application/json": { + "serviceId": "Host.exampleId.localhost.deviceconnect.org", + "profile": "mediastreamrecording", + "attribute": "onphoto", + "photo": { + "uri": "Example URI", + "path": "Example Path", + "mimeType": "image/png" + } + } + } + } + }, + "delete": { + "operationId": "mediaStreamRecordingOnPhotoDelete", + "x-type": "event", + "summary": "スマートデバイスの写真撮影通知イベントを停止する。", + "description": "スマートデバイスでサポートしていないパラメータがある場合には、 そのパラメータを省略する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "query", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "通知イベントの停止結果を返す。 停止に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/EventUnregistrationResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + }, + "/preview": { + "get": { + "operationId": "mediaStreamRecordingPreviewGet", + "x-type": "one-shot", + "summary": "スマートデバイスでの動画撮影中に、動画のプレビュー画像を取得する。", + "description": "このイベント通知を行うタイミングに規定は無く、 デバイスプラグインの実装依存とする。
JPEGでデータを受信する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "query", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "query", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "ストリーミング映像のURIを返す。 映像のURIを返せない場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/PreviewGetResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0", + "uri": "Example Preview URI" + } + } + } + } + }, + "put": { + "operationId": "mediaStreamRecordingPreviewPut", + "x-type": "streaming", + "summary": "スマートデバイスでの動画撮影中に、動画のプレビュー画像を受信を開始する。", + "description": "このイベント通知を行うタイミングに規定は無く、 デバイスプラグインの実装依存とする。MotionJPEGでデータを受信する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "ストリーミング映像のURIを返す。 映像のURIを返せない場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/PreviewStartResponse" + }, + "examples": { + "application/json": { + "streams": [ + { + "mimeType": "video/x-mjpeg", + "uri": "http://localhost:9000/xxxxxx" + }, + { + "mimeType": "video/x-rtp", + "uri": "rtsp://localhost:8086" + } + ], + "result": 0, + "product": "Example System", + "version": "1.0.0", + "uri": "Example Preview URI" + } + } + } + } + }, + "delete": { + "operationId": "mediaStreamRecordingPreviewDelete", + "x-type": "streaming", + "summary": "スマートデバイスでの動画撮影中に、動画のプレビュー画像を受信を停止する。", + "description": "このイベント通知を行うタイミングに規定は無く、 デバイスプラグインの実装依存とする。
MotionJPEGなどでデータを受信する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "query", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "query", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "ストリーミング映像のURIを返す。 映像のURIを返せない場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/PreviewStopResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + }, + "/preview/mute": { + "put": { + "operationId": "mediaStreamRecordingPreviewMutePut", + "x-type": "one-shot", + "summary": "音声が付加されているPreview映像に対し、 その映像の音声をミュート状態にするリクエストを送る。", + "description": "例えば、Preview映像がRTSPの場合に、このAPIにより音声をミュート状態にする。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Previewの音声をミュート状態にするリクエストを送った結果を返す。 ミュートリクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + }, + "delete": { + "operationId": "mediaStreamRecordingPreviewMuteDelete", + "x-type": "one-shot", + "summary": "音声が付加されているPreview映像に対し、 その映像の音声のミュート状態を解除するリクエストを送る。", + "description": "例えば、Preview映像がRTSPの場合に、このAPIにより音声のミュート状態を解除する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Previewの音声のミュート状態を解除するリクエストを送った結果を返す。 ミュート解除リクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + }, + "/preview/requestKeyFrame": { + "post": { + "operationId": "mediaStreamRecordingPreviewRequestKeyFramePost", + "x-type": "one-shot", + "summary": "プレビューのキーフレーム送信を要求する。", + "description": "プレビューのエンコーダに対して映像のキーフレームを生成・送信するように要求する。
1つのターゲットについて複数の映像形式をサポートする場合、起動中のすべてのエンコーダに対して要求する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "formData", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "プレビューのキーフレーム送信リクエストの結果を返す。 失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + }, + "/broadcast": { + "put": { + "operationId": "mediaStreamRecordingBroadcastPut", + "x-type": "one-shot", + "summary": "スマートデバイスでの動画を指定された URI に配信を開始する。", + "description": "スマートデバイスでの動画を指定された URI に配信を開始する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "query", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "uri", + "description": "配信先のサーバURI。
rtmp://、rtmps://、srt:// のプロトコルに対応", + "in": "query", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "配信開始の結果を返す。", + "schema": { + "$ref": "#/definitions/BroadcastStartResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + }, + "delete": { + "operationId": "mediaStreamRecordingBroadcastDelete", + "x-type": "streaming", + "summary": "スマートデバイスでの撮影している映像の配信を停止する。", + "description": "スマートデバイスでの撮影している映像の配信を停止する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "query", + "required": true, + "type": "string" + }, + { + "name": "target", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "in": "query", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "映像の配信停止の結果を返す。", + "schema": { + "$ref": "#/definitions/BroadcastStopResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + }, + "/onRecordingChange": { + "get": { + "operationId": "mediaStreamRecordingOnRecordingChangeGet", + "x-type": "one-shot", + "summary": "スマートデバイスでの写真撮影、動画撮影または音声録音の状態変化通知イベントを 取得する。", + "description": "プラグイン側でキャッシュしている最新のイベントメッセージを1つ取得する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "query", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "撮影または録音の状態を返す。 状態を返せない場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecordingStatusResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0", + "media": { + "status": "stop", + "path": "Example Path", + "mimeType": "video/3gp" + } + } + } + } + } + }, + "put": { + "operationId": "mediaStreamRecordingOnRecordingChangePut", + "x-type": "event", + "summary": "スマートデバイスでの写真撮影、動画撮影または音声録音の状態変化通知イベントを 開始する。", + "description": "スマートデバイスでサポートしていないパラメータがある場合には、 そのパラメータを省略する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "formData", + "required": true, + "type": "string" + }, + { + "name": "interval", + "description": "デバイスプラグインがイベントを送信する間隔。", + "in": "formData", + "required": false, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "撮影または録音の状態変化通知イベントの開始結果を返す。 開始に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/EventRegistrationResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-event": { + "schema": { + "$ref": "#/definitions/RecordingStatusEvent" + }, + "examples": { + "application/json": { + "serviceId": "Host.exampleId.localhost.deviceconnect.org", + "profile": "mediastreamrecording", + "attribute": "onrecordingchange", + "media": { + "status": "stop", + "path": "Example Path", + "mimeType": "image/png" + } + } + } + } + }, + "delete": { + "operationId": "mediaStreamRecordingOnRecordingChangeDelete", + "x-type": "event", + "summary": "スマートデバイスでの写真撮影、動画撮影または音声録音の状態変化通知イベントを 停止する。", + "description": "スマートデバイスでサポートしていないパラメータがある場合には、 そのパラメータを省略する。", + "parameters": [ + { + "name": "serviceId", + "description": "サービスID。取得対象スマートデバイス", + "in": "query", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "撮影または録音の状態変化通知イベントの停止結果を返す。 停止に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/EventUnregistrationResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + } + } + } + }, + "definitions": { + "MediaRecorderResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "required": [ + "recorders" + ], + "properties": { + "recorders": { + "type": "array", + "title": "レコーダーリスト", + "description": "レコーダー情報の配列。", + "items": { + "type": "object", + "required": [ + "id", + "name", + "state", + "mimeType", + "config" + ], + "properties": { + "id": { + "type": "string", + "title": "レコーダーID", + "description": "レコーダーを識別するID。" + }, + "name": { + "type": "string", + "title": "レコーダー名", + "description": "レコーダー名。" + }, + "state": { + "type": "string", + "title": "レコーダーの状態", + "description": "レコーダーの状態。", + "enum": [ + "inactive", + "recording", + "paused" + ] + }, + "mimeType": { + "type": "string", + "title": "MIME Type", + "description": "レコーダーのエンコードするタイプ。" + }, + "config": { + "type": "string", + "title": "コンフィグ", + "description": "カメラに設定がある場合には、ここに文字列としてデータが格納されている。" + }, + "imageWidth": { + "type": "integer", + "title": "画像の横幅", + "description": "レコーダーの現在の横幅。単位はピクセル。録音の場合は省略可。" + }, + "imageHeight": { + "type": "integer", + "title": "画像の縦幅", + "description": "レコーダーの現在の縦幅。単位はピクセル。録音の場合は省略可。" + }, + "previewWidth": { + "type": "integer", + "title": "プレビューの横幅", + "description": "プレビューの現在の横幅。単位はピクセル。録音の場合は省略可。" + }, + "previewHeight": { + "type": "integer", + "title": "プレビューの縦幅", + "description": "プレビューの現在の縦幅。単位はピクセル。録音の場合は省略可。" + }, + "previewMaxFrameRate": { + "type": "number", + "title": "プレビューの最大フレームレート", + "description": "現在のプレビューのフレームレートの最大値。単位はfps。
録音の場合は省略可。" + }, + "previewKeyFrameInterval": { + "type": "integer", + "title": "プレビューのキーフレーム送信間隔", + "description": "現在のプレビューのキーフレーム送信間隔。単位はミリ秒。
録音の場合は省略可。" + }, + "previewEncoderBitRate": { + "type": "integer", + "title": "プレビューのエンコーダのビットレート設定", + "description": "現在のプレビューのエンコーダのビットレート設定。エンコーダへの設定のため、実際の出力ビットレートはエンコーダに依存する。単位はbps。
録音の場合は省略可。" + }, + "audio": { + "type": "object", + "title": "音声情報", + "description": "録音する音声に関する情報。", + "required": [ + "channels", + "sampleRate", + "sampleSize", + "blockSize" + ], + "properties": { + "channels": { + "type": "integer", + "title": "チャンネル数", + "description": "音声のチャンネル数。" + }, + "sampleRate": { + "type": "number", + "title": "サンプルレート", + "description": "音声のサンプルレート。単位はHz。" + }, + "sampleSize": { + "type": "integer", + "title": "サンプルサイズ", + "description": "音声のサンプルサイズ。単位はビット。" + }, + "blockSize": { + "type": "integer", + "title": "ブロックサイズ", + "description": "音声のブロックサイズ。単位はバイト。" + } + } + } + } + } + } + } + } + ] + }, + "TakePhotoResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "required": [ + "uri", + "path" + ], + "properties": { + "uri": { + "type": "string", + "title": "URI", + "description": "撮影された写真のURI。" + }, + "path": { + "type": "string", + "title": "ファイルパス", + "description": "撮影された写真へのファイルパス。ルートはデバイスプラグインごとに異なる。
File APIのパラメータとして使用可能。" + } + } + } + ] + }, + "RecordResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "properties": { + "uri": { + "type": "string", + "title": "URI", + "description": "動画または音声のURI。" + }, + "path": { + "type": "string", + "title": "ファイルパス", + "description": "動画または音声へのファイルパス。ルートはデバイスプラグインごとに異なる。
File APIのパラメータとして使用可能。" + } + } + } + ] + }, + "OptionsResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "required": [ + "mimeType" + ], + "properties": { + "mimeType": { + "type": "array", + "title": "MIME Type", + "description": "録画・録音する際のエンコードするタイプの一覧", + "items": { + "type": "string" + } + }, + "imageSizes": { + "type": "array", + "title": "撮影時の解像度の一覧", + "description": "レコーダーのサポートする画像の解像度の一覧。録音の場合は省略可。
プラグイン側でリサイズすることでサポートするサイズも含めてよい。", + "items": { + "type": "object", + "title": "解像度", + "description": "画像の解像度。", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "title": "横幅", + "description": "画像の横幅。単位はピクセル。" + }, + "height": { + "type": "integer", + "title": "縦幅", + "description": "画像の縦幅。単位はピクセル。" + } + } + } + }, + "previewSizes": { + "type": "array", + "title": "プレビュー時の解像度の一覧", + "description": "プレビューで利用可能な解像度の一覧。
録音の場合、またはプレビューを提供しない場合は省略可。
プラグイン側でリサイズすることでサポートするサイズも含めてよい。", + "items": { + "type": "object", + "title": "解像度", + "description": "画像の解像度。", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "title": "横幅", + "description": "画像の横幅。単位はピクセル。" + }, + "height": { + "type": "integer", + "title": "縦幅", + "description": "画像の縦幅。単位はピクセル。" + } + } + } + } + } + } + ] + }, + "PhotoResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "$ref": "#/definitions/PhotoInfo" + } + ] + }, + "PhotoEvent": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonEvent" + }, + { + "$ref": "#/definitions/PhotoInfo" + } + ] + }, + "PhotoInfo": { + "type": "object", + "required": [ + "photo" + ], + "properties": { + "photo": { + "type": "object", + "title": "写真データ", + "description": "撮影された写真データ。", + "required": [ + "path", + "mimeType" + ], + "properties": { + "uri": { + "type": "string", + "title": "URI", + "description": "撮影された写真のURI。" + }, + "path": { + "type": "string", + "title": "ファイルパス", + "description": "ファイルが存在するパス。ルートはデバイスプラグインごとに異なる。
File APIで使用可能。" + }, + "mimeType": { + "type": "string", + "title": "MIME Type", + "description": "撮影された写真のMIME Type。動画・音声を識別するために使用する。" + } + } + } + } + }, + "PreviewGetResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "title": "プレビュー画像URI", + "description": "プレビュー画像URI。形式はJPEGとする。" + } + } + } + ] + }, + "PreviewStartResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "title": "プレビュー配信URI", + "description": "開始したプレビューの配信用URI。形式はMotionJPEGとする。" + }, + "streams": { + "type": "array", + "title": "ストリームのリスト", + "description": "ストリーム情報の配列。", + "items": { + "type": "object", + "required": [ + "mimeType", + "uri" + ], + "properties": { + "mimeType": { + "type": "string", + "title": "ストリームのMIMEType", + "description": "ストリームのMIMEType。" + }, + "uri": { + "type": "string", + "title": "ストリームのURI", + "description": "ストリームのURI。" + } + } + } + }, + "audio": { + "type": "object", + "title": "音声配信情報", + "description": "音声配信に関する情報。音声のみの配信をサポートしない場合は省略可。", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "title": "音声配信URI", + "description": "音声配信URI。" + } + } + } + } + } + ] + }, + "PreviewStopResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + } + ] + }, + "BroadcastStartResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + } + ] + }, + "BroadcastStopResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + } + ] + }, + "RecordingStatusResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "$ref": "#/definitions/RecordingStatusInfo" + } + ] + }, + "RecordingStatusEvent": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonEvent" + }, + { + "$ref": "#/definitions/RecordingStatusInfo" + } + ] + }, + "RecordingStatusInfo": { + "type": "object", + "required": [ + "media" + ], + "properties": { + "media": { + "type": "object", + "title": "レコーディング情報", + "description": "レコーディング情報", + "required": [ + "status", + "mimeType" + ], + "properties": { + "status": { + "type": "string", + "title": "レコーディングの状態", + "description": "レコーディングの状態を識別する文字列。", + "enum": [ + "recording", + "stop", + "pause", + "resume", + "mutetrack", + "unmutetrack", + "error", + "warning" + ] + }, + "uri": { + "type": "string", + "title": "URI", + "description": "動画または音声のURI。" + }, + "path": { + "type": "string", + "title": "ファイルパス", + "description": "ファイルが存在するパス。ルートはデバイスプラグインごとに違う。" + }, + "mimeType": { + "type": "string", + "title": "MIME Type", + "description": "録画・録音が開始されたメディアのMIME Type。
このタイプで、動画、音声などを識別する。" + }, + "errorMessasge": { + "type": "string", + "title": "エラーメッセージ", + "description": "エラー、警告内容を伝える文字列。
状態が error、warning の時のみ付加される。省略可能。" + } + } + } + } + }, + "RecorderStopResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "properties": { + "uri": { + "type": "string", + "title": "URI", + "description": "動画または音声のURI。" + }, + "path": { + "type": "string", + "title": "ファイルパス", + "description": "動画または音声へのファイルパス。
ルートはデバイスプラグインごとに異なる。
File APIのパラメータとして使用可能。" + } + } + } + ] + }, + "RecorderControlResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + } + ] + }, + "EventRegistrationResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + } + ] + }, + "EventUnregistrationResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + } + ] + }, + "CommonResponse": { + "type": "object", + "required": [ + "result", + "product", + "version" + ], + "properties": { + "result": { + "type": "integer", + "title": "処理結果", + "description": "0: 正常応答
0以外: 異常応答" + }, + "product": { + "type": "string", + "title": "システム名", + "description": "DeviceConnectシステムの名前。" + }, + "version": { + "type": "string", + "title": "システムバージョン", + "description": "DeviceConnectシステムのバージョン名。" + }, + "hmac": { + "type": "string", + "title": "署名", + "description": "レスポンスに対する署名。
アプリケーション側から事前にHMACキーを共有されていた場合は必須。" + } + } + }, + "CommonEvent": { + "type": "object", + "required": [ + "serviceId", + "profile", + "interface", + "attribute" + ], + "properties": { + "serviceId": { + "type": "string", + "title": "サービスID", + "description": "イベントを送信したサービスのID" + }, + "profile": { + "type": "string", + "title": "プロファイル名", + "description": "プロファイル名。" + }, + "interface": { + "type": "string", + "title": "インターフェース名", + "description": "インターフェース名。" + }, + "attribute": { + "type": "string", + "title": "アトリビュート名", + "description": "アトリビュート名。" + } + } + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/UVCDeviceApplication.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/UVCDeviceApplication.java deleted file mode 100644 index 32ed400ed8..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/UVCDeviceApplication.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - UVCDeviceApplication.java - Copyright (c) 2015 NTT DOCOMO,INC. - Released under the MIT license - http://opensource.org/licenses/mit-license.php - */ -package org.deviceconnect.android.deviceplugin.uvc; - -import android.app.Application; - -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDeviceManager; -import org.deviceconnect.android.logger.AndroidHandler; - -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; - -/** - * UVC Device Application. - * - * @author NTT DOCOMO, INC. - */ -public class UVCDeviceApplication extends Application { - - private final Logger mLogger = Logger.getLogger("uvc.dplugin"); - - private UVCDeviceManager mDeviceMgr; - - @Override - public void onCreate() { - super.onCreate(); - - if (BuildConfig.DEBUG) { - AndroidHandler handler = new AndroidHandler("uvc.dplugin"); - handler.setFormatter(new SimpleFormatter()); - handler.setLevel(Level.ALL); - mLogger.addHandler(handler); - mLogger.setLevel(Level.ALL); - } else { - mLogger.setLevel(Level.OFF); - } - - mDeviceMgr = new UVCDeviceManager(getApplicationContext()); - } - - public UVCDeviceManager getDeviceManager() { - return mDeviceMgr; - } - -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/UVCDeviceService.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/UVCDeviceService.java index 172cd376da..ed6c6f932e 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/UVCDeviceService.java +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/UVCDeviceService.java @@ -6,13 +6,9 @@ */ package org.deviceconnect.android.deviceplugin.uvc; - import android.Manifest; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.hardware.usb.UsbManager; +import android.content.SharedPreferences; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.wifi.WifiManager; @@ -21,13 +17,18 @@ import android.util.Log; import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; import org.deviceconnect.android.activity.PermissionUtility; -import org.deviceconnect.android.deviceplugin.uvc.activity.ErrorDialogActivity; -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDevice; -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDeviceManager; import org.deviceconnect.android.deviceplugin.uvc.profile.UVCSystemProfile; +import org.deviceconnect.android.deviceplugin.uvc.recorder.PreviewServer; +import org.deviceconnect.android.deviceplugin.uvc.recorder.uvc.UvcRecorder; import org.deviceconnect.android.deviceplugin.uvc.service.UVCService; +import org.deviceconnect.android.deviceplugin.uvc.util.UVCRegistry; +import org.deviceconnect.android.libusb.UsbSerialPortManager; +import org.deviceconnect.android.libuvc.UVCCamera; +import org.deviceconnect.android.libuvc.UVCCameraManager; +import org.deviceconnect.android.libuvc.utils.WeakReferenceList; import org.deviceconnect.android.message.DConnectMessageService; import org.deviceconnect.android.profile.SystemProfile; import org.deviceconnect.android.service.DConnectService; @@ -57,101 +58,49 @@ public class UVCDeviceService extends DConnectMessageService { private final Logger mLogger = Logger.getLogger("uvc.dplugin"); - private UVCDeviceManager mDeviceMgr; + /** + * デフォルトのパスワードを定義します. + */ + private static final String DEFAULT_PASSWORD = "0000"; + /** + * SSL コンテキスト. + */ private SSLContext mSSLContext; + /** - * SSLContext を提供するインターフェース. + * USB 管理クラス. */ - public interface SSLContextCallback { - void onGet(SSLContext context); - void onError(); - } + private UsbSerialPortManager mUsbSerialPortManager; - public void getSSLContext(final SSLContextCallback callback) { - final SSLContext sslContext = mSSLContext; - if (sslContext != null) { - mLogger.log(Level.INFO, "getSSLContext: requestKeyStore: onSuccess: Already created SSL Context: " + sslContext); - callback.onGet(sslContext); - } else { - requestKeyStore(getIPAddress(this), new KeyStoreCallback() { - public void onSuccess(final KeyStore keyStore, final Certificate certificate, final Certificate certificate1) { - try { - mLogger.log(Level.INFO, "getSSLContext: requestKeyStore: onSuccess: Creating SSL Context..."); - mSSLContext = createSSLContext(keyStore, "0000"); - mLogger.log(Level.INFO, "getSSLContext: requestKeyStore: onSuccess: Created SSL Context: " + mSSLContext); - callback.onGet(mSSLContext); - } catch (GeneralSecurityException e) { - mLogger.log(Level.WARNING, "getSSLContext: requestKeyStore: onSuccess: Failed to create SSL Context", e); - callback.onError(); - } - } + /** + * UVC カメラを管理するクラス. + */ + private UVCCameraManager mUVCCameraManager; - public void onError(final KeyStoreError keyStoreError) { - mLogger.warning("getSSLContext: requestKeyStore: onError: error = " + keyStoreError); - callback.onError(); - } - }); - } - } - private BroadcastReceiver mPermissionReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - PermissionUtility.requestPermissions(context, - new Handler(Looper.getMainLooper()), - new String[]{Manifest.permission.CAMERA}, - new PermissionUtility.PermissionRequestCallback() { - - @Override - public void onSuccess() { - mDeviceMgr = ((UVCDeviceApplication) getApplication()).getDeviceManager(); - mDeviceMgr.addDeviceListener(mDeviceListener); - mDeviceMgr.addConnectionListener(mConnectionListener); - mDeviceMgr.start(); - } + /** + * UVC のリスト管理クラス. + */ + private UVCRegistry mUVCRegistry; - @Override - public void onFail(@NonNull String s) { + /** + * イベントを通知するリスナーを格納するリスト. + */ + private final WeakReferenceList mOnEventListeners = new WeakReferenceList<>(); - } - }); - } - }; @Override public void onCreate() { super.onCreate(); - registerReceiver(mPermissionReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED)); - } - @Override - protected boolean usesAutoCertificateRequest() { - return true; + setUseLocalOAuth(checkUseLocalOAuth()); + init(); + initUVCCameraManager(); } - @Override - protected void onKeyStoreUpdated(final KeyStore keyStore, final Certificate cert, final Certificate rootCert) { - try { - if (keyStore == null || getPluginContext() == null) { - return; - } - mSSLContext = createSSLContext(keyStore, "0000"); - } catch (GeneralSecurityException e) { - mLogger.log(Level.SEVERE, "Failed to update keystore", e); - } - } @Override public void onDestroy() { - for (DConnectService service : getServiceProvider().getServiceList()) { - if (service instanceof UVCService) { - ((UVCService) service).closeUVCDevice(); - } - } - if (mDeviceMgr != null) { - mDeviceMgr.removeDeviceListener(mDeviceListener); - mDeviceMgr.removeConnectionListener(mConnectionListener); - mDeviceMgr.stop(); - } - unregisterReceiver(mPermissionReceiver); super.onDestroy(); + disposeUVCCameraManager(); + mOnEventListeners.clear(); } @Override @@ -169,27 +118,14 @@ protected void onManagerTerminated() { mLogger.info("Plug-in : onManagerTerminated"); } } + @Override protected void onDevicePluginReset() { // Device Plug-inへのReset要求受信時の処理。 if (BuildConfig.DEBUG) { mLogger.info("Plug-in : onDevicePluginReset"); } - PermissionUtility.requestPermissions(this, - new Handler(Looper.getMainLooper()), - new String[]{Manifest.permission.CAMERA}, - new PermissionUtility.PermissionRequestCallback() { - - @Override - public void onSuccess() { - resetPluginResource(); - } - - @Override - public void onFail(@NonNull String s) { - - } - }); + resetPluginResource(); } @Override @@ -197,70 +133,247 @@ protected SystemProfile getSystemProfile() { return new UVCSystemProfile(); } + @Override + protected String getCertificateAlias() { + return "org.deviceconnect.android.deviceplugin.uvc"; + } + + @Override + protected boolean usesAutoCertificateRequest() { + // SSL の証明書を使用するので true を返却 + return true; + } + + @Override + protected void onKeyStoreUpdated(final KeyStore keyStore, final Certificate cert, final Certificate rootCert) { + try { + if (keyStore == null || getPluginContext() == null) { + return; + } + mSSLContext = createSSLContext(keyStore, DEFAULT_PASSWORD); + } catch (GeneralSecurityException e) { + mLogger.log(Level.SEVERE, "Failed to update keystore", e); + } + } + + @Override + public void setUseLocalOAuth(boolean a) { + super.setUseLocalOAuth(a); + } + + /** + * イベントを通知するリスナーを追加します. + * + * 追加したリスナーは、{@link #removeOnEventListener(OnEventListener)} で削除してください。 + * + * @param listener 追加するリスナー + */ + public void addOnEventListener(OnEventListener listener) { + mOnEventListeners.add(listener); + } + + /** + * イベントを通知するリスナーを削除します. + * + * @param listener 削除するリスナー + */ + public void removeOnEventListener(OnEventListener listener) { + mOnEventListeners.remove(listener); + } + + /** + * UVC の接続イベントを通知します. + * + * @param service 接続した UVC サービス + */ + private void postOnConnected(UVCService service) { + for (OnEventListener l : mOnEventListeners) { + l.onConnected(service); + } + } + + /** + * UVC の切断イベントを通知します. + * + * @param service 切断した UVC サービス + */ + private void postOnDisconnected(UVCService service) { + for (OnEventListener l : mOnEventListeners) { + l.onDisconnected(service); + } + } + + /** + * 初期化処理を行います. + */ + private void init() { + mUVCRegistry = new UVCRegistry(this); + + for (UVCRegistry.UVC uvc : mUVCRegistry.getUVCList()) { + UVCService service = new UVCService(getApplicationContext(), uvc.getDeviceId()); + service.setName(uvc.getName()); + service.setOnline(false); + getServiceProvider().addService(service); + } + } + /** * リソースリセット処理. */ private void resetPluginResource() { for (DConnectService service : getServiceProvider().getServiceList()) { - if (service instanceof UVCService) { - ((UVCService) service).reset(); + if (service instanceof UVCService) { + ((UVCService) service).disconnect(); } } + disposeUVCCameraManager(); + initUVCCameraManager(); } - private final UVCDeviceManager.DeviceListener mDeviceListener = new UVCDeviceManager.DeviceListener() { - @Override - public void onFound(final UVCDevice device) { - if (mDeviceMgr.connectDevice(device)) { - if (!device.canPreview()) { - ErrorDialogActivity.showNotSupportedError(getApplicationContext(), device); + /** + * USBを管理するクラスを初期化します. + */ + private void initUVCCameraManager() { + mUsbSerialPortManager = new UsbSerialPortManager(this); + mUVCCameraManager = new UVCCameraManager(mUsbSerialPortManager); + mUVCCameraManager.setOnEventListener(new UVCCameraManager.OnEventListener() { + @Override + public void onConnected(final UVCCamera uvcCamera) { + connectUVCCamera(uvcCamera); + } + + @Override + public void onDisconnected(final UVCCamera uvcCamera) { + disconnectUVCCamera(uvcCamera); + } + + @Override + public void onError(final Exception e) { + if (BuildConfig.DEBUG) { + Log.w("UVC", "error", e); } - } else { - mLogger.severe("UVC device COULD NOT be initialized: " + device.getName()); } - } - }; - - private final UVCDeviceManager.ConnectionListener mConnectionListener = new UVCDeviceManager.ConnectionListener() { - @Override - public void onConnect(final UVCDevice device) { - UVCService service = getService(device); - if (service != null) { - service.openUVCDevice(device); - service.setOnline(true); + + @Override + public void onRequestPermission(UsbSerialPortManager.PermissionCallback callback) { + PermissionUtility.requestPermissions(getApplicationContext(), + new Handler(Looper.getMainLooper()), + new String[]{Manifest.permission.CAMERA}, + new PermissionUtility.PermissionRequestCallback() { + @Override + public void onSuccess() { + callback.allow(); + } + + @Override + public void onFail(@NonNull String s) { + callback.deny(); + } + }); } + }); + mUVCCameraManager.startMonitoring(); + } + + /** + * USBを管理するクラスの後始末を行います. + */ + private void disposeUVCCameraManager() { + if (mUVCCameraManager != null) { + mUVCCameraManager.stopMonitoring(); + mUVCCameraManager.dispose(); + mUVCCameraManager = null; } + if (mUsbSerialPortManager != null) { + mUsbSerialPortManager.stopUsbMonitoring(); + mUsbSerialPortManager.dispose(); + mUsbSerialPortManager = null; + } + } - @Override - public void onConnectionFailed(final UVCDevice device) { - // NOP. + private synchronized void connectUVCCamera(UVCCamera uvcCamera) { + UVCService service = findUVCServiceByUVC(uvcCamera); + if (service == null) { + service = new UVCService(getApplicationContext(), createUVCServiceId(uvcCamera)); + service.connect(uvcCamera); + getServiceProvider().addService(service); + mUVCRegistry.addUVC(service.getId(), service.getName()); + } else { + service.connect(uvcCamera); } + setSSLContext(service); + postOnConnected(service); + } - @Override - public void onDisconnect(final UVCDevice device) { - if (device != null) { - UVCService service = (UVCService) getServiceProvider().getService(device.getId()); - if (service != null) { - service.closeUVCDevice(); - service.setOnline(false); - } + private synchronized void disconnectUVCCamera(UVCCamera uvcCamera) { + UVCService service = findUVCServiceByUVC(uvcCamera); + if (service != null) { + service.disconnect(); + } + postOnDisconnected(service); + } + + private String createUVCServiceId(UVCCamera uvcCamera) { + return "uvc-" + uvcCamera.getDeviceId(); + } + + private UVCService findUVCServiceByUVC(UVCCamera uvcCamera) { + return findUVCServiceById(createUVCServiceId(uvcCamera)); + } + + public UVCService findUVCServiceById(String id) { + if (id != null) { + DConnectService service = getServiceProvider().getService(id); + if (service instanceof UVCService) { + return (UVCService) service; } } - }; + return null; + } - private UVCService addService(final UVCDevice device) { - UVCService service = new UVCService(mDeviceMgr, device); - getServiceProvider().addService(service); - return service; + public UVCService getActiveUVCService() { + for (DConnectService service : getServiceProvider().getServiceList()) { + if (service instanceof UVCService && service.isOnline()) { + return (UVCService) service; + } + } + return null; } - private UVCService getService(final UVCDevice device) { - UVCService service = (UVCService) getServiceProvider().getService(device.getId()); - if (service == null) { - service = addService(device); + // SSL + + private void setSSLContext(UVCService service) { + final SSLContext sslContext = mSSLContext; + if (sslContext != null) { + setSSLContext(service, sslContext); + } else { + requestKeyStore(getIPAddress(this), new KeyStoreCallback() { + @Override + public void onSuccess(final KeyStore keyStore, final Certificate certificate, final Certificate certificate1) { + try { + mSSLContext = createSSLContext(keyStore, DEFAULT_PASSWORD); + setSSLContext(service, mSSLContext); + } catch (Exception e) { + // ignore. + } + } + @Override + public void onError(final KeyStoreError keyStoreError) { + } + }); + } + } + + private void setSSLContext(UVCService service, SSLContext sslContext) { + for (UvcRecorder recorder : service.getUvcRecorderList()) { + for (PreviewServer server : recorder.getServerProvider().getServers()) { + if (server.useSSLContext()) { + server.setSSLContext(sslContext); + } + } } - return service; } + /** * Gets the ip address. * @@ -274,23 +387,22 @@ public static String getIPAddress(final Context context) { NetworkInfo network = cManager.getActiveNetworkInfo(); String en0Ip = null; if (network != null) { - switch (network.getType()) { - case ConnectivityManager.TYPE_ETHERNET: - try { - for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { - NetworkInterface intf = en.nextElement(); - for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { - InetAddress inetAddress = enumIpAddr.nextElement(); - if (inetAddress instanceof Inet4Address - && !inetAddress.getHostAddress().equals("127.0.0.1")) { - en0Ip = inetAddress.getHostAddress(); - break; - } + if (network.getType() == ConnectivityManager.TYPE_ETHERNET) { + try { + for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { + NetworkInterface intf = en.nextElement(); + for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { + InetAddress inetAddress = enumIpAddr.nextElement(); + if (inetAddress instanceof Inet4Address + && !inetAddress.getHostAddress().equals("127.0.0.1")) { + en0Ip = inetAddress.getHostAddress(); + break; } } - } catch (SocketException e) { - Log.e("Host", "Get Ethernet IP Error", e); } + } catch (SocketException e) { + // ignore. + } } } @@ -303,4 +415,33 @@ public static String getIPAddress(final Context context) { (ipAddress >> 16 & 0xff), (ipAddress >> 24 & 0xff)); } } + + /** + * ユーザ認可の設定を取得します. + * + * @return ユーザ認可の設定 + */ + private boolean checkUseLocalOAuth() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + return prefs.getBoolean("uvc_settings_auth", false); + } + + /** + * UVC 接続・切断イベントを通知するリスナー. + */ + public interface OnEventListener { + /** + * UVC が接続したことを通知します. + * + * @param service 接続した UVC サービス + */ + void onConnected(UVCService service); + + /** + * UVC が切断されたことを通知します. + * + * @param service 切断された UVC サービス + */ + void onDisconnected(UVCService service); + } } diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/ErrorDialogActivity.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/ErrorDialogActivity.java deleted file mode 100644 index ce939c2ce4..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/ErrorDialogActivity.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - UVCDeviceErrorActivity.java - Copyright (c) 2015 NTT DOCOMO,INC. - Released under the MIT license - http://opensource.org/licenses/mit-license.php - */ -package org.deviceconnect.android.deviceplugin.uvc.activity; - - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.Window; - -import org.deviceconnect.android.deviceplugin.uvc.R; -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDevice; - -public class ErrorDialogActivity extends Activity { - - public static final String PARAM_TITLE = "title"; - public static final String PARAM_MESSAGE = "message"; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - requestWindowFeature(Window.FEATURE_NO_TITLE); - } - - @Override - protected void onResume() { - super.onResume(); - - Intent intent = getIntent(); - if (intent == null) { - finish(); - } - String title = intent.getStringExtra(PARAM_TITLE); - String message = intent.getStringExtra(PARAM_MESSAGE); - if (title == null || message == null) { - finish(); - } - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(title); - builder.setMessage(message); - builder.setCancelable(true); - builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { - finish(); - }); - builder.setOnCancelListener((dialogInterface) -> { - finish(); - }); - builder.create().show(); - } - - public static void show(final Context context, final String title, final String message) { - Intent intent = new Intent(context, ErrorDialogActivity.class); - intent.putExtra(PARAM_TITLE, title); - intent.putExtra(PARAM_MESSAGE, message); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - - public static void show(final Context context, final String message) { - show(context, context.getString(R.string.uvc_error_dialog_default_title), message); - } - - public static void showNotSupportedError(final Context context, final UVCDevice device) { - String baseMessage = context.getString(R.string.uvc_error_message_device_not_supported); - String message = baseMessage.replace("{NAME}", device.getName()); - show(context, message); - } -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCDevicePluginBindActivity.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCDevicePluginBindActivity.java new file mode 100644 index 0000000000..09f8fa8e55 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCDevicePluginBindActivity.java @@ -0,0 +1,222 @@ +package org.deviceconnect.android.deviceplugin.uvc.activity; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; + +import org.deviceconnect.android.deviceplugin.uvc.UVCDeviceService; +import org.deviceconnect.android.deviceplugin.uvc.service.UVCService; + +public abstract class UVCDevicePluginBindActivity extends AppCompatActivity { + /** + * UVC プラグイン. + */ + private UVCDeviceService mUVCDeviceService; + + /** + * 接続状態を確認するフラグ. + */ + private boolean mIsBound = false; + + /** + * UVC サービスの接続・切断イベントを受信するリスナー. + */ + private final UVCDeviceService.OnEventListener mOnEventListener = new UVCDeviceService.OnEventListener() { + @Override + public void onConnected(UVCService service) { + onUvcConnected(service); + } + + @Override + public void onDisconnected(UVCService service) { + onUvcDisconnected(service); + } + }; + + @Override + public void onResume() { + super.onResume(); + bindService(); + } + + @Override + protected void onDestroy() { + unbindService(); + super.onDestroy(); + } + + /** + * UVC プラグインを取得します. + * + * 接続されていない場合は null を返却します。 + * + * @return UVC プラグイン + */ + public UVCDeviceService getUVCDeviceService() { + return mUVCDeviceService; + } + + /** + * UVCDeviceService との接続状態を確認します. + * + * @return 接続されている場合は true、それ以外は false + */ + public boolean isBound() { + return mIsBound && mUVCDeviceService != null; + } + + /** + * UVCDeviceService に接続されたことを通知します. + * + * UVCDeviceService に接続された時に処理を行う場合には、このメソッドをオーバーライドします。 + */ + protected void onBindService() { + for (Fragment f : getSupportFragmentManager ().getFragments()) { + if (f instanceof NavHostFragment) { + for (Fragment t : f.getChildFragmentManager().getFragments()) { + if (t instanceof OnUVCDevicePluginListener) { + ((OnUVCDevicePluginListener) t).onBindService(); + } + } + } else if (f instanceof OnUVCDevicePluginListener) { + ((OnUVCDevicePluginListener) f).onBindService(); + } + } + } + + /** + * UVCDeviceService から切断されたことを通知します. + */ + protected void onUnbindService() { + for (Fragment f : getSupportFragmentManager ().getFragments()) { + if (f instanceof NavHostFragment) { + for (Fragment t : f.getChildFragmentManager().getFragments()) { + if (t instanceof OnUVCDevicePluginListener) { + ((OnUVCDevicePluginListener) t).onUnbindService(); + } + } + } else if (f instanceof OnUVCDevicePluginListener) { + ((OnUVCDevicePluginListener) f).onUnbindService(); + } + } + } + + /** + * UVC サービスに接続されたことを通知します. + * + * @param service 接続された UVC サービス + */ + protected void onUvcConnected(UVCService service) { + for (Fragment f : getSupportFragmentManager ().getFragments()) { + if (f instanceof NavHostFragment) { + for (Fragment t : f.getChildFragmentManager().getFragments()) { + if (t instanceof OnUVCDevicePluginListener) { + ((OnUVCDevicePluginListener) t).onUvcConnected(service); + } + } + } else if (f instanceof OnUVCDevicePluginListener) { + ((OnUVCDevicePluginListener) f).onUvcConnected(service); + } + } + } + + /** + * UVC サービスから切断されたことを通知します. + * + * @param service 切断された UVC サービス + */ + protected void onUvcDisconnected(UVCService service) { + for (Fragment f : getSupportFragmentManager ().getFragments()) { + if (f instanceof NavHostFragment) { + for (Fragment t : f.getChildFragmentManager().getFragments()) { + if (t instanceof OnUVCDevicePluginListener) { + ((OnUVCDevicePluginListener) t).onUvcDisconnected(service); + } + } + } else if (f instanceof OnUVCDevicePluginListener) { + ((OnUVCDevicePluginListener) f).onUvcDisconnected(service); + } + } + } + + /** + * UVCDeviceService に接続します. + */ + public synchronized void bindService() { + if (mIsBound) { + return; + } + mIsBound = true; + + Intent intent = new Intent(getApplicationContext(), UVCDeviceService.class); + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + /** + * UVCDeviceService から切断します. + */ + public synchronized void unbindService() { + if (mIsBound) { + mIsBound = false; + if (mUVCDeviceService != null) { + mUVCDeviceService.removeOnEventListener(mOnEventListener); + } + onUnbindService(); + unbindService(mConnection); + } + } + + private final ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + mUVCDeviceService = (UVCDeviceService) ((UVCDeviceService.LocalBinder) binder).getMessageService(); + mUVCDeviceService.addOnEventListener(mOnEventListener); + onBindService(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + if (mUVCDeviceService != null) { + mUVCDeviceService.removeOnEventListener(mOnEventListener); + } + mUVCDeviceService = null; + mIsBound = false; + onUnbindService(); + } + }; + + /** + * UVCDeviceService の接続イベントを通知するリスナー. + */ + public interface OnUVCDevicePluginListener { + /** + * UVCDeviceService に接続されたことを通知します. + */ + void onBindService(); + + /** + * UVCDeviceService から切断されたことを通知します. + */ + void onUnbindService(); + + /** + * UVC サービスに接続したことを通知します. + * + * @param service UVC サービス + */ + void onUvcConnected(UVCService service); + + /** + * UVC サービスから切断されたことを通知します. + * + * @param service UVC サービス + */ + void onUvcDisconnected(UVCService service); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCDeviceSettingsActivity.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCDeviceSettingsActivity.java deleted file mode 100644 index 84e48b3546..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCDeviceSettingsActivity.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - UVCDeviceSettingsActivity.java - Copyright (c) 2015 NTT DOCOMO,INC. - Released under the MIT license - http://opensource.org/licenses/mit-license.php - */ -package org.deviceconnect.android.deviceplugin.uvc.activity; - - -import androidx.fragment.app.Fragment; - -import org.deviceconnect.android.deviceplugin.uvc.fragment.UVCDeviceInstructionFragment; -import org.deviceconnect.android.deviceplugin.uvc.fragment.UVCDeviceListFragment; -import org.deviceconnect.android.ui.activity.DConnectSettingPageFragmentActivity; - - -public class UVCDeviceSettingsActivity extends DConnectSettingPageFragmentActivity { - - @Override - public int getPageCount() { - return 2; - } - - @Override - public Fragment createPage(int position) { - if (position == 0) { - return new UVCDeviceInstructionFragment(); - } else { - return new UVCDeviceListFragment(); - } - } - -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCServiceListActivity.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCServiceListActivity.java deleted file mode 100644 index 1e1c88b75c..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCServiceListActivity.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - UVCServiceListActivity.java - Copyright (c) 2015 NTT DOCOMO,INC. - Released under the MIT license - http://opensource.org/licenses/mit-license.php - */ -package org.deviceconnect.android.deviceplugin.uvc.activity; - - -import android.app.Activity; - -import org.deviceconnect.android.deviceplugin.uvc.UVCDeviceService; -import org.deviceconnect.android.message.DConnectMessageService; -import org.deviceconnect.android.ui.activity.DConnectServiceListActivity; - - -/** - * UVCサービス一覧画面. - * - * @author NTT DOCOMO, INC. - */ -public class UVCServiceListActivity extends DConnectServiceListActivity { - - @Override - protected Class getMessageServiceClass() { - return UVCDeviceService.class; - } - - @Override - protected Class getSettingManualActivityClass() { - return UVCDeviceSettingsActivity.class; - } -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCSettingsActivity.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCSettingsActivity.java new file mode 100644 index 0000000000..2d02cfd4f3 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/activity/UVCSettingsActivity.java @@ -0,0 +1,31 @@ +package org.deviceconnect.android.deviceplugin.uvc.activity; + +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.appcompat.app.ActionBar; + +import org.deviceconnect.android.deviceplugin.uvc.R; + +public class UVCSettingsActivity extends UVCDevicePluginBindActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_uvc_settings); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setTitle(""); + } + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/core/UVCDevice.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/core/UVCDevice.java deleted file mode 100644 index 246d1afa4a..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/core/UVCDevice.java +++ /dev/null @@ -1,481 +0,0 @@ -/* - UVCDevice.java - Copyright (c) 2015 NTT DOCOMO,INC. - Released under the MIT license - http://opensource.org/licenses/mit-license.php - */ -package org.deviceconnect.android.deviceplugin.uvc.core; - - -import android.hardware.usb.UsbDevice; -import android.view.Surface; -import android.view.TextureView; - -import com.serenegiant.usb.Size; -import com.serenegiant.usb.USBMonitor; -import com.serenegiant.usb.UVCCamera; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.logging.Logger; - -public class UVCDevice { - - private static final double DEFAULT_MAX_FPS = 30.0d; - - private static final int VS_FORMAT_MJPEG = 0x06; - - private static final int[] SUPPORTED_PAYLOAD_FORMATS = { - VS_FORMAT_MJPEG - }; - - private final Logger mLogger = Logger.getLogger("uvc.dplugin"); - - private final UsbDevice mDevice; - - private final UVCDeviceManager mDeviceMgr; - - private USBMonitor.UsbControlBlock mCtrlBlock; - - private UVCCamera mCamera; - - private final String mId; - - private final List mPreviewListeners = new ArrayList(); - - private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); - - final Object mLockPermission = new Object(); - - private boolean mIsPermitted; - - private boolean mIsInitialized; - - private boolean mIsOpen; - - private boolean mHasStartedPreview; - - private PreviewOption mCurrentOption; - - private long mMinFrameInterval; - - private double mMaxFps; - - private long mLastFrameTime = -1; - - private PendingPermissionRequest mPermissionRequest; - - UVCDevice(final UsbDevice device, final UVCDeviceManager deviceMgr) { - mDevice = device; - mId = Integer.toString(device.getDeviceId()); - mDeviceMgr = deviceMgr; - setPreviewFrameRate(DEFAULT_MAX_FPS); - } - - public String getId() { - return mId; - } - - public String getName() { - return mDevice.getDeviceName(); - } - - public int getVendorId() { - return mDevice.getVendorId(); - } - - public int getProductId() { - return mDevice.getProductId(); - } - - public boolean isOpen() { - return mIsOpen; - } - - public boolean isInitialized() { - return mIsInitialized; - } - - public boolean canPreview() { - return mCurrentOption != null; - } - - public boolean hasStartedPreview() { - return mHasStartedPreview; - } - - boolean isSameDevice(final UsbDevice usbDevice) { - return usbDevice.getDeviceName().equals(mDevice.getDeviceName()); - } - - void requestPermission() throws InterruptedException { - synchronized (mLockPermission) { - if (mPermissionRequest == null) { - mPermissionRequest = new PendingPermissionRequest(); - } - mDeviceMgr.requestPermission(mDevice); - while (mPermissionRequest != null - && mPermissionRequest.isWaitingResult()) { - mLockPermission.wait(100); - } - mIsPermitted = mPermissionRequest.isPermitted(); - mLogger.info("requestPermission: isPermitted = " + mIsPermitted); - mPermissionRequest = null; - } - } - - void notifyPermission(final USBMonitor.UsbControlBlock ctrlBlock) { - mLogger.info("notifyPermission: ctrlBlock = " + ctrlBlock); - synchronized (mLockPermission) { - if (mPermissionRequest != null) { - mPermissionRequest.setResult(ctrlBlock != null); - mCtrlBlock = ctrlBlock; - } - mLockPermission.notifyAll(); - } - } - - synchronized boolean connect() { - if (mIsInitialized) { - return true; - } - if (!mIsPermitted) { - try { - mLogger.info("Requesting permission..."); - requestPermission(); - mLogger.info("Received the response for permission request: result = " + mIsPermitted); - } catch (InterruptedException e) { - return false; - } - } - if (!open()) { // Check supported video formats. - return false; - } - mLogger.info("UVC device: name = " + getName() + ", supported format = " + mCamera.getSupportedSize()); - mIsInitialized = true; - return true; - } - - private boolean open() { - if (mIsOpen) { - return true; - } - if (!mIsPermitted) { - return false; - } - - mCamera = new UVCCamera(); - mCamera.open(mCtrlBlock); - mIsOpen = true; - - List previewSizeList = mCamera.getSupportedSizeList(); - mLogger.info("Supported preview sizes: " + previewSizeList.size()); - Size size = selectSize(previewSizeList); - if (size == null) { - mLogger.warning("Preview size fof supported format (MJPEG or YUY2) is not found."); - return false; - } - mLogger.info("Selected Preview size: type = " + size.type + ", width = " + size.width + ", height = " + size.height); - if (mCurrentOption == null) { - mCurrentOption = new PreviewOption(size.width, size.height); - } - final int width = mCurrentOption.getWidth(); - final int height = mCurrentOption.getHeight(); - final int frameFormat = UVCCamera.FRAME_FORMAT_MJPEG; - final int pixelFormat = UVCCamera.PIXEL_FORMAT_RAW; - - if (!setPreviewSize(width, height)) { - mIsOpen = false; - mCurrentOption = null; - return false; - } - mCamera.setPreviewFrameCallback((frame) -> { - if (checkFrameInterval()) { - return; - } - notifyPreviewFrame(frame, frameFormat, width, height); - }, pixelFormat); - - return true; - } - - private boolean supportsFormat(final Size previewSize) { - for (int format : SUPPORTED_PAYLOAD_FORMATS) { - if (previewSize.type == format) { - return true; - } - } - return false; - } - - private Size selectSize(final List sizeList) { - if (sizeList.size() == 0) { - return null; - } - for (int format : SUPPORTED_PAYLOAD_FORMATS) { - Size size = selectSize(sizeList, format); - if (size != null) { - return size; - } - } - return null; - } - - private Size selectSize(final List sizeList, final int format) { - List list = new ArrayList<>(); - int i = 0; - for (Size size : sizeList) { - if (size.type == format) { - list.add(size); - } - } - if (list.size() == 0) { - return null; - } - Collections.sort(list, (s1, s2) -> { - return -1 * (s2.width * s2.height - s1.width * s1.height); //最小 - }); - return list.get(0); - } - - private boolean checkFrameInterval() { - if (mMinFrameInterval <= 0) { - return false; - } - long currentFrameTime = System.currentTimeMillis(); - if (mLastFrameTime < 0 || currentFrameTime - mLastFrameTime >= mMinFrameInterval) { - mLastFrameTime = currentFrameTime; - return false; - } - return true; - } - - private void notifyPreviewFrame(final byte[] frame, final int frameFormat, - final int width, final int height) { - synchronized (mPreviewListeners) { - for (Iterator it = mPreviewListeners.iterator(); it.hasNext(); ) { - final PreviewListener l = it.next(); - mExecutor.execute(() -> { - l.onFrame(UVCDevice.this, frame, frameFormat, width, height); - }); - } - } - } - - synchronized boolean disconnect() { - if (!mIsInitialized) { - return false; - } - if (!mIsOpen) { - return false; - } - stopPreview(); - mCamera.close(); - mCamera.destroy(); - mCamera = null; - mCtrlBlock.close(); - mCtrlBlock = null; - mCurrentOption = null; - mIsOpen = false; - mIsPermitted = false; - mIsInitialized = false; - return true; - } - - void addPreviewListener(final PreviewListener listener) { - synchronized (mPreviewListeners) { - for (Iterator it = mPreviewListeners.iterator(); it.hasNext(); ) { - if (it.next() == listener) { - return; - } - } - mPreviewListeners.add(listener); - } - } - - public boolean setPreviewSize(final int width, final int height) { - if (!isSupportedPreviewSize(width, height)) { - return false; - } - try { - if (mIsOpen) { - mCamera.setPreviewSize(width, height, UVCCamera.FRAME_FORMAT_MJPEG); - } - mCurrentOption = new PreviewOption(width, height); - return true; - } catch (IllegalArgumentException e) { - return false; - } - } - - public double getFrameRate() { - return mMaxFps; - } - - public void setPreviewFrameRate(final double maxFrameRate) { - mMaxFps = maxFrameRate; - mMinFrameInterval = (long) (1000 / maxFrameRate); - } - - public boolean setNearestPreviewSize(final int requestedWidth, - final int requestedHeight) { - PreviewOption option = getNearestPreviewSize(requestedWidth, requestedHeight); - return setPreviewSize(option.getWidth(), option.getHeight()); - } - - public PreviewOption getNearestPreviewSize(final int requestedWidth, - final int requestedHeight) { - List options = getPreviewOptions(); - final float ratio = requestedWidth / requestedHeight; - final int area = requestedWidth * requestedHeight; - Collections.sort(options, (op1, op2) -> { - if (op1.getRatio() == op2.getRatio()) { - int d1 = Math.abs(area - op1.getWidth() * op2.getHeight()); - int d2 = Math.abs(area - op2.getWidth() * op2.getHeight()); - return d1 - d2; - } else { - float d1 = Math.abs(ratio - op1.getRatio()); - float d2 = Math.abs(ratio - op2.getRatio()); - return d1 > d2 ? 1 : d1 == d2 ? 0 : -1; - } - }); - return options.get(0); - } - - public void setPreviewDisplay(final TextureView display) { - Surface surface = new Surface(display.getSurfaceTexture()); - mCamera.setPreviewDisplay(surface); - } - - public void clearPreviewDisplay() { - mCamera.setPreviewDisplay((Surface) null); - } - - private void clearPreviewListeners() { - synchronized (mPreviewListeners) { - mPreviewListeners.clear(); - } - } - - public synchronized boolean startPreview() { - if (!mIsOpen) { - mLogger.warning("UVCDevice.startPreview: device is not open. name = " + getName()); - return false; - } - if (mHasStartedPreview) { - mLogger.info("UVCDevice.startPreview: preview is started already. name = " + getName()); - return true; - } - mLogger.info("UVCDevice.startPreview: preview is starting... name = " + getName()); - mCamera.startPreview(); - mLogger.info("UVCDevice.startPreview: preview has started. name = " + getName()); - mHasStartedPreview = true; - return true; - } - - public synchronized boolean stopPreview() { - if (!mIsOpen) { - mLogger.warning("UVCDevice.stopPreview: device is not open. name = " + getName()); - return false; - } - if (!mHasStartedPreview) { - mLogger.info("UVCDevice.stopPreview: preview is stopped already. name = " + getName()); - return true; - } - mLogger.info("UVCDevice.stopPreview: preview is stopping... name = " + getName()); - mCamera.stopPreview(); - mLogger.info("UVCDevice.stopPreview: preview has stopped. name = " + getName()); - mHasStartedPreview = false; - return true; - } - - public int getPreviewWidth() { - return mCurrentOption.getWidth(); - } - - public int getPreviewHeight() { - return mCurrentOption.getHeight(); - } - - public synchronized List getPreviewOptions() { - if (!mIsOpen) { - return null; - } - List options = new ArrayList<>(); - List supportedSizes = mCamera.getSupportedSizeList(); - for (Size size : supportedSizes) { - if (!supportsFormat(size)) { - continue; - } - options.add(new PreviewOption(size)); - } - return options; - } - - private boolean isSupportedPreviewSize(final int width, final int height) { - List sizes = mCamera.getSupportedSizeList(); - for (Size size : sizes) { - if (width == size.width && height == size.height) { - return true; - } - } - return false; - } - - private static class PendingPermissionRequest { - - Boolean mIsPermitted; - - boolean isWaitingResult() { - return mIsPermitted == null; - } - - void setResult(final boolean isPermitted) { - mIsPermitted = isPermitted; - } - - boolean isPermitted() { - return mIsPermitted; - } - - } - - interface PreviewListener { - - void onFrame(UVCDevice device, byte[] frame, int frameFormat, int width, int height); - - } - - public static class PreviewOption { - - private final int mWidth; - - private final int mHeight; - - private PreviewOption(final int width, final int height) { - mWidth = width; - mHeight = height; - } - - private PreviewOption(final Size size) { - this(size.width, size.height); - } - - public int getWidth() { - return mWidth; - } - - public int getHeight() { - return mHeight; - } - - public float getRatio() { - return mWidth / mHeight; - } - } - -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/core/UVCDeviceManager.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/core/UVCDeviceManager.java deleted file mode 100644 index eb200c51fc..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/core/UVCDeviceManager.java +++ /dev/null @@ -1,453 +0,0 @@ -/* - UVCDeviceManager.java - Copyright (c) 2015 NTT DOCOMO,INC. - Released under the MIT license - http://opensource.org/licenses/mit-license.php - */ -package org.deviceconnect.android.deviceplugin.uvc.core; - - -import android.content.Context; -import android.hardware.usb.UsbDevice; -import android.os.Build; - -import com.serenegiant.usb.DeviceFilter; -import com.serenegiant.usb.USBMonitor; - -import org.deviceconnect.android.deviceplugin.uvc.R; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.logging.Logger; - -public class UVCDeviceManager { - - private static final long INTERVAL_MONITORING = 3000; // 3 seconds - - private final Logger mLogger = Logger.getLogger("uvc.dplugin"); - - private final USBMonitor mUSBMonitor; - - private final List mAttachedDevices = new ArrayList<>(); - - private final List mDeviceListeners = new ArrayList<>(); - - private final List mConnectionListeners = new ArrayList<>(); - - private final List mDiscoveryListeners = new ArrayList<>(); - - private final List mPreviewListeners = new ArrayList<>(); - - private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); - - private final UVCDevice.PreviewListener mPreviewListener = this::notifyPreviewFrame; - - private boolean mIsStarted; - - private Thread mScanThread; - - private int mScanNum; - - public UVCDeviceManager(final Context context) { - mUSBMonitor = new USBMonitor(context, new USBMonitor.OnDeviceConnectListener() { - @Override - public void onAttach(final UsbDevice usbDevice) { - if (!supportedAttachEvent()) { - return; - } - if (usbDevice == null) { - return; - } - mLogger.info("onAttach: " + usbDevice.getDeviceName()); - - UVCDevice device = getDevice(usbDevice); - if (device == null) { - device = new UVCDevice(usbDevice, UVCDeviceManager.this); - device.addPreviewListener(mPreviewListener); - pushDevice(device); - notifyEventOnFound(device); - } - } - - @Override - public void onDettach(final UsbDevice usbDevice) { - mLogger.info("onDettach: " + usbDevice.getDeviceName()); - pullDevice(usbDevice); - } - - @Override - public void onConnect(final UsbDevice usbDevice, - final USBMonitor.UsbControlBlock ctrlBlock, - final boolean createNew) { - mLogger.info("onConnect: device = " + usbDevice.getDeviceName() - + ", ctrlBlock = " + ctrlBlock + ", createNew = " + createNew); - - UVCDevice device = getDevice(usbDevice); - if (device != null) { - device.notifyPermission(ctrlBlock); - } - } - - @Override - public void onDisconnect(final UsbDevice usbDevice, - final USBMonitor.UsbControlBlock ctrlBlock) { - mLogger.info("onDisconnect: " + usbDevice.getDeviceName()); - final UVCDevice device = getDevice(usbDevice); - mExecutor.execute(() -> { - if (device != null && device.disconnect()) { - notifyEventOnDisconnect(device); - } - }); - } - - @Override - public void onCancel(final UsbDevice usbDevice) { - mLogger.info("onCancel"); - UVCDevice device = getDevice(usbDevice); - if (device != null) { - device.notifyPermission(null); - } - } - }); - - List filters = DeviceFilter.getDeviceFilters(context, R.xml.device_filter); - mUSBMonitor.setDeviceFilter(filters); - } - - void requestPermission(final UsbDevice usbDevice) { - mUSBMonitor.requestPermission(usbDevice); - } - - public boolean connectDevice(final UVCDevice device) { - mLogger.info("Connecting Device... : " + device.getName()); - if (device.connect()) { - notifyEventOnConnect(device); - return true; - } else { - notifyEventOnConnectionFailed(device); - return false; - } - } - - public boolean connectDevice(final String id) { - UVCDevice device = getDevice(id); - if (device != null) { - return connectDevice(device); - } else { - mLogger.warning("connectDevice: unknown device: " + device.getName()); - return false; - } - } - - public void disconnectDevice(final UVCDevice device) { - if (device.disconnect()) { - mLogger.info("disconnectDevice: closed " + device.getName()); - notifyEventOnDisconnect(device); - } else { - mLogger.info("disconnectDevice: already closed " + device.getName()); - } - } - - public void disconnectDevice(final String id) { - UVCDevice device = getDevice(id); - if (device != null) { - disconnectDevice(device); - } else { - mLogger.warning("disconnectDevice: unknown device: " + device.getName()); - } - } - - private void pushDevice(final UVCDevice device) { - synchronized (mAttachedDevices) { - mAttachedDevices.add(device); - } - } - - private UVCDevice pullDevice(final UsbDevice usbDevice) { - synchronized (mAttachedDevices) { - for (Iterator it = mAttachedDevices.iterator(); it.hasNext(); ) { - UVCDevice device = it.next(); - if (device.isSameDevice(usbDevice)) { - it.remove(); - return device; - } - } - } - return null; - } - - private UVCDevice getDevice(final UsbDevice usbDevice) { - synchronized (mAttachedDevices) { - for (Iterator it = mAttachedDevices.iterator(); it.hasNext(); ) { - UVCDevice device = it.next(); - if (device.isSameDevice(usbDevice)) { - return device; - } - } - } - return null; - } - - public void addDeviceListener(final DeviceListener listener) { - synchronized (mDeviceListeners) { - mDeviceListeners.add(listener); - } - } - - public void removeDeviceListener(final DeviceListener listener) { - synchronized (mDeviceListeners) { - for (Iterator it = mDeviceListeners.iterator(); it.hasNext(); ) { - DeviceListener l = it.next(); - if (l == listener) { - it.remove(); - return; - } - } - } - } - - private void notifyEventOnFound(final UVCDevice device) { - synchronized (mDeviceListeners) { - for (Iterator it = mDeviceListeners.iterator(); it.hasNext(); ) { - final DeviceListener l = it.next(); - mExecutor.execute(() -> { - l.onFound(device); - }); - } - } - } - - public void addConnectionListener(final ConnectionListener listener) { - synchronized (mConnectionListeners) { - mConnectionListeners.add(listener); - } - } - - public void removeConnectionListener(final ConnectionListener listener) { - synchronized (mConnectionListeners) { - for (Iterator it = mConnectionListeners.iterator(); it.hasNext(); ) { - ConnectionListener l = it.next(); - if (l == listener) { - it.remove(); - return; - } - } - } - } - - private void notifyEventOnConnect(final UVCDevice device) { - synchronized (mConnectionListeners) { - for (Iterator it = mConnectionListeners.iterator(); it.hasNext(); ) { - final ConnectionListener l = it.next(); - mExecutor.execute(() -> { - l.onConnect(device); - }); - } - } - } - - private void notifyEventOnConnectionFailed(final UVCDevice device) { - synchronized (mConnectionListeners) { - for (Iterator it = mConnectionListeners.iterator(); it.hasNext(); ) { - final ConnectionListener l = it.next(); - mExecutor.execute(() -> { - l.onConnectionFailed(device); - }); - } - } - } - - private void notifyEventOnDisconnect(final UVCDevice device) { - synchronized (mConnectionListeners) { - for (Iterator it = mConnectionListeners.iterator(); it.hasNext(); ) { - final ConnectionListener l = it.next(); - mExecutor.execute(() -> { - l.onDisconnect(device); - }); - } - } - } - - public void addDiscoveryListener(final DiscoveryListener listener) { - synchronized (mDiscoveryListeners) { - mDiscoveryListeners.add(listener); - } - } - - public void removeDiscoveryListener(final DiscoveryListener listener) { - synchronized (mDiscoveryListeners) { - for (Iterator it = mDiscoveryListeners.iterator(); it.hasNext(); ) { - DiscoveryListener l = it.next(); - if (l == listener) { - it.remove(); - return; - } - } - } - } - - private void notifyEventOnDiscovery(final List devices) { - synchronized (mDiscoveryListeners) { - for (Iterator it = mDiscoveryListeners.iterator(); it.hasNext(); ) { - final DiscoveryListener l = it.next(); - mExecutor.execute(() -> { - l.onDiscovery(devices); - }); - } - } - } - - public void addPreviewListener(final PreviewListener listener) { - synchronized (mPreviewListeners) { - for (Iterator it = mPreviewListeners.iterator(); it.hasNext(); ) { - if (it.next() == listener) { - return; - } - } - mPreviewListeners.add(listener); - } - } - - public void removePreviewListener(final PreviewListener listener) { - synchronized (mPreviewListeners) { - for (Iterator it = mPreviewListeners.iterator(); it.hasNext(); ) { - PreviewListener l = it.next(); - if (l == listener) { - it.remove(); - return; - } - } - } - } - - private void clearPreviewListeners() { - synchronized (mPreviewListeners) { - mPreviewListeners.clear(); - } - } - - private void notifyPreviewFrame(final UVCDevice device, final byte[] frame, final int frameFormat, - final int width, final int height) { - synchronized (mPreviewListeners) { - for (Iterator it = mPreviewListeners.iterator(); it.hasNext(); ) { - final PreviewListener l = it.next(); - mExecutor.execute(() -> { - l.onFrame(device, frame, frameFormat, width, height); - }); - } - } - } - - public synchronized void start() { - if (mIsStarted) { - return; - } - mIsStarted = true; - - mUSBMonitor.register(); - if (supportedAttachEvent()) { - doScanOnce(); - } else { - startScan(); - } - } - - public synchronized void startScan() { - if (mScanThread == null) { - mScanThread = new Thread(() -> { - mLogger.info("Started UVC device monitoring: "); - - // Find UVC devices connected to Host device already. - try { - do { - doScanOnce(); - Thread.sleep(INTERVAL_MONITORING); - } while (mIsStarted); - } catch (InterruptedException e) { - // Nothing to do. - } - mLogger.info("Stopped UVC device monitoring."); - }); - mScanThread.start(); - } - ++mScanNum; - } - - private void doScanOnce() { - List usbDevices = mUSBMonitor.getDeviceList(); - notifyEventOnDiscovery(getDeviceList()); - - for (UsbDevice usbDevice : usbDevices) { - if (getDevice(usbDevice) != null) { - continue; - } - UVCDevice device = new UVCDevice(usbDevice, UVCDeviceManager.this); - device.addPreviewListener(mPreviewListener); - pushDevice(device); - notifyEventOnFound(device); - } - } - - public synchronized void stopScan() { - if (mScanThread != null) { - --mScanNum; - if (mScanNum == 0) { - mScanThread.interrupt(); - mScanThread = null; - } - } - } - - private boolean supportedAttachEvent() { - return Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1; - } - - public synchronized void stop() { - if (!mIsStarted) { - return; - } - stopScan(); - mUSBMonitor.unregister(); - mIsStarted = false; - } - - public List getDeviceList() { - return mAttachedDevices; - } - - public UVCDevice getDevice(final String id) { - synchronized (mAttachedDevices) { - for (UVCDevice device : mAttachedDevices) { - if (device.getId().equals(id)) { - return device; - } - } - } - return null; - } - - public USBMonitor getUSBMonitor() { - return mUSBMonitor; - } - - public interface DeviceListener { - void onFound(UVCDevice device); - } - - public interface ConnectionListener { - void onConnect(UVCDevice device); - void onConnectionFailed(UVCDevice device); - void onDisconnect(UVCDevice device); - } - - public interface DiscoveryListener { - void onDiscovery(List devices); - } - - public interface PreviewListener { - void onFrame(UVCDevice device, byte[] frame, int frameFormat, int width, int height); - } -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/PermissionConfirmationFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/PermissionConfirmationFragment.java new file mode 100644 index 0000000000..45772f8a8d --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/PermissionConfirmationFragment.java @@ -0,0 +1,97 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.Manifest; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; + +import org.deviceconnect.android.deviceplugin.uvc.R; +import org.deviceconnect.android.deviceplugin.uvc.databinding.FragmentUvcPermissionConfirmationBinding; +import org.deviceconnect.android.libmedia.streaming.util.PermissionUtil; + +import java.util.List; + +import static androidx.navigation.fragment.NavHostFragment.findNavController; + +public abstract class PermissionConfirmationFragment extends UVCDevicePluginBindFragment { + /** + * パーミッションのリクエストコード. + */ + private static final int PERMISSION_REQUEST_CODE = 21234; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + FragmentUvcPermissionConfirmationBinding binding = DataBindingUtil.inflate(inflater, + R.layout.fragment_uvc_permission_confirmation, container, false); + binding.setPresenter(this); + return binding.getRoot(); + } + + @Override + public void onResume() { + super.onResume(); + + Context context = getContext(); + if (context == null) { + return; + } + + List denies = PermissionUtil.checkPermissions(context, getPermissions()); + if (denies.isEmpty()) { + onNextFragment(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == PERMISSION_REQUEST_CODE) { + List denies = PermissionUtil.checkRequestPermissionsResult(permissions, grantResults); + if (denies.isEmpty()) { + onNextFragment(); + } else { + onPermissionDeny(); + } + } + } + + /** + * パーミッションを要求します. + */ + public void onClickRequestPermissionsButton() { + Context context = getContext(); + if (context == null) { + return; + } + + List denies = PermissionUtil.checkPermissions(context, getPermissions()); + if (!denies.isEmpty()) { + requestPermissions(denies.toArray(new String[0]), PERMISSION_REQUEST_CODE); + } + } + + /** + * 許可を求めるパーミッションの配列を取得します. + * + * @return パーミッションの配列 + */ + public String[] getPermissions() { + return new String[] { + Manifest.permission.CAMERA + }; + } + + /** + * パーミッションの許可が降りている場合に次の画面に遷移します. + */ + public abstract void onNextFragment(); + + /** + * パーミッションの許可が降りなかった場合の処理を行います. + */ + public abstract void onPermissionDeny(); +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDeviceInstructionFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDeviceInstructionFragment.java index 6431bf3586..bf49e27d48 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDeviceInstructionFragment.java +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDeviceInstructionFragment.java @@ -11,20 +11,16 @@ import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import org.deviceconnect.android.deviceplugin.uvc.R; - -public class UVCDeviceInstructionFragment extends Fragment { - - @Nullable +public class UVCDeviceInstructionFragment extends UVCDevicePluginBindFragment { @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.fragment_uvc_device_instruction, null); - return root; + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + setTitle(getString(R.string.uvc_settings_title_uvc_device_instruction)); + return inflater.inflate(R.layout.fragment_uvc_device_instruction, null); } - } diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDeviceListFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDeviceListFragment.java index 841c36f641..be24d363bc 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDeviceListFragment.java +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDeviceListFragment.java @@ -6,421 +6,163 @@ */ package org.deviceconnect.android.deviceplugin.uvc.fragment; - import android.app.Activity; import android.content.Context; -import android.content.res.Resources; +import android.graphics.Color; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.ListView; -import android.widget.TextView; -import androidx.fragment.app.Fragment; +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; import org.deviceconnect.android.deviceplugin.uvc.R; -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDevice; -import org.deviceconnect.android.deviceplugin.uvc.UVCDeviceApplication; -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDeviceManager; -import org.deviceconnect.android.deviceplugin.uvc.fragment.dialog.ErrorDialogFragment; -import org.deviceconnect.android.deviceplugin.uvc.fragment.dialog.ProgressDialogFragment; +import org.deviceconnect.android.deviceplugin.uvc.UVCDeviceService; +import org.deviceconnect.android.deviceplugin.uvc.activity.UVCSettingsActivity; +import org.deviceconnect.android.deviceplugin.uvc.databinding.FragmentUvcDeviceListBinding; +import org.deviceconnect.android.deviceplugin.uvc.databinding.ItemUvcDeviceBinding; +import org.deviceconnect.android.deviceplugin.uvc.service.UVCService; +import org.deviceconnect.android.service.DConnectService; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.logging.Logger; -import static org.deviceconnect.android.deviceplugin.uvc.core.UVCDeviceManager.ConnectionListener; -import static org.deviceconnect.android.deviceplugin.uvc.core.UVCDeviceManager.DiscoveryListener; +import static androidx.navigation.fragment.NavHostFragment.create; +import static androidx.navigation.fragment.NavHostFragment.findNavController; -public class UVCDeviceListFragment extends Fragment { - /** - * Adapter. - */ +public class UVCDeviceListFragment extends UVCDevicePluginBindFragment { private DeviceAdapter mDeviceAdapter; - /** - * Error Dialog. - */ - private ErrorDialogFragment mErrorDialogFragment; - - /** - * Progress Dialog. - */ - private ProgressDialogFragment mProgressDialogFragment; - - /** - * UVC device list view. - */ - private ListView mListView; + private UVCDeviceService mUVCDeviceService; - /** - * Footer view. - */ - private View mFooterView; - - /** - * Logger. - */ - private final Logger mLogger = Logger.getLogger("uvc.dplugin"); + private final UVCDeviceService.OnEventListener mOnEventListener = new UVCDeviceService.OnEventListener() { + @Override + public void onConnected(UVCService service) { + if (mDeviceAdapter != null) { + mDeviceAdapter.setContainers(createDeviceContainerList()); + } + } - /** - * Executor. - */ - private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + @Override + public void onDisconnected(UVCService service) { + if (mDeviceAdapter != null) { + mDeviceAdapter.notifyDataSetChanged(); + } + } + }; @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - mDeviceAdapter = new DeviceAdapter(getActivity(), createDeviceContainers()); - - mFooterView = inflater.inflate(R.layout.item_uvc_error, null); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + FragmentUvcDeviceListBinding binding = DataBindingUtil.inflate(inflater, + R.layout.fragment_uvc_device_list, container, false); + binding.setPresenter(this); - View rootView = inflater.inflate(R.layout.fragment_uvc_device_list, null); - mListView = rootView.findViewById(R.id.device_list_view); - mListView.setAdapter(mDeviceAdapter); - mListView.setItemsCanFocus(true); - return rootView; - } + mDeviceAdapter = new DeviceAdapter(getContext(), new ArrayList<>()); + setTitle(getString(R.string.uvc_settings_title_uvc_device_list)); - @Override - public void onResume() { - super.onResume(); - addFooterView(); - getManager().startScan(); - getManager().addConnectionListener(mConnectionListener); - getManager().addDiscoveryListener(mDiscoverListener); + View rootView = binding.getRoot(); + ListView listView = rootView.findViewById(R.id.device_list_view); + listView.setAdapter(mDeviceAdapter); + listView.setItemsCanFocus(true); + return rootView; } @Override - public void onPause() { - super.onPause(); - getManager().removeConnectionListener(mConnectionListener); - getManager().removeDiscoveryListener(mDiscoverListener); - getManager().stopScan(); - dismissProgressDialog(); - dismissErrorDialog(); - } - - /** - * Added the view at ListView. - */ - private void addFooterView() { - final Activity activity = getActivity(); - if (activity == null) { - return; - } - activity.runOnUiThread(() -> { - UVCDeviceManager mgr = getManager(); - if (mgr == null) { - return; - } - LayoutInflater inflater = activity.getLayoutInflater(); - if (mFooterView != null) { - mListView.removeFooterView(mFooterView); - } - if (mgr.getDeviceList().size() == 0) { - mFooterView = inflater.inflate(R.layout.item_uvc_error, null); - mListView.addFooterView(mFooterView); - } - }); - } - - /** - * Connect to the UVC device that have heart rate service. - * - * @param device UVC device that have heart rate service. - */ - private void connectDevice(final DeviceContainer device) { - showProgressDialog(device.getName()); - mExecutor.execute(() -> { - getManager().connectDevice(device.getId()); - }); - } - - /** - * Disconnect to the UVC device that have heart rate service. - * - * @param device UVC device that have heart rate service. - */ - private void disconnectDevice(final DeviceContainer device) { - mExecutor.execute(() -> { - getManager().disconnectDevice(device.getId()); - }); - } - - /** - * Display the dialog of connecting a ble device. - * - * @param name device name - */ - private void showProgressDialog(final String name) { - dismissProgressDialog(); - - Resources res = getActivity().getResources(); - String title = res.getString(R.string.uvc_settings_connecting_title); - String message = res.getString(R.string.uvc_settings_connecting_message, name); - mProgressDialogFragment = ProgressDialogFragment.newInstance(title, message); - mProgressDialogFragment.show(getFragmentManager(), "dialog"); - } - - /** - * Dismiss the dialog of connecting a ble device. - */ - private void dismissProgressDialog() { - if (mProgressDialogFragment != null) { - mProgressDialogFragment.dismiss(); - mProgressDialogFragment = null; - } - } - - /** - * Display the error dialog of not connect device. - * - * @param name device name - */ - private void showErrorDialogNotConnect(final String name) { - Resources res = getActivity().getResources(); - String message; - if (name == null) { - message = res.getString(R.string.uvc_settings_dialog_error_message, - getString(R.string.uvc_settings_default_name)); - } else { - message = res.getString(R.string.uvc_settings_dialog_error_message, name); + public void onBindService() { + mUVCDeviceService = getUVCDeviceService(); + if (mUVCDeviceService != null) { + mUVCDeviceService.addOnEventListener(mOnEventListener); } - showErrorDialog(message); + mDeviceAdapter.setContainers(createDeviceContainerList()); } - /** - * Display the error dialog. - * - * @param message error message - */ - public void showErrorDialog(final String message) { - dismissErrorDialog(); - - Resources res = getActivity().getResources(); - String title = res.getString(R.string.uvc_settings_dialog_error_title); - mErrorDialogFragment = ErrorDialogFragment.newInstance(title, message); - mErrorDialogFragment.show(getFragmentManager(), "error_dialog"); - mErrorDialogFragment.setOnDismissListener((dialog) -> { - mErrorDialogFragment = null; - }); - } - - /** - * Dismiss the error dialog. - */ - private void dismissErrorDialog() { - if (mErrorDialogFragment != null) { - mErrorDialogFragment.dismiss(); - mErrorDialogFragment = null; - } - } - - /** - * Gets a instance of UVCDeviceManager. - * - * @return UVCDeviceManager - */ - private UVCDeviceManager getManager() { - Activity activity = getActivity(); - if (activity == null) { - return null; + @Override + public void onUnbindService() { + if (mUVCDeviceService != null) { + mUVCDeviceService.removeOnEventListener(mOnEventListener); } - UVCDeviceApplication application = - (UVCDeviceApplication) activity.getApplication(); - return application.getDeviceManager(); } - private ConnectionListener mConnectionListener = new ConnectionListener() { - - @Override - public void onConnect(final UVCDevice device) { - if (getActivity() == null) { - return; - } - getActivity().runOnUiThread(() -> { - DeviceContainer container = findDeviceContainerById(device.getId()); - if (container != null) { - container.setRegisterFlag(true); - mDeviceAdapter.notifyDataSetChanged(); - } - dismissProgressDialog(); - }); - } - - @Override - public void onConnectionFailed(final UVCDevice device) { - if (getActivity() == null) { - return; - } - getActivity().runOnUiThread(() -> { - dismissProgressDialog(); - showErrorDialogNotConnect(device.getName()); - }); - } - - @Override - public void onDisconnect(final UVCDevice device) { - if (getActivity() == null) { - return; - } - getActivity().runOnUiThread(() -> { - DeviceContainer container = findDeviceContainerById(device.getId()); - if (container != null) { - container.setRegisterFlag(false); - mDeviceAdapter.notifyDataSetChanged(); - } - dismissProgressDialog(); - }); - } - }; - - private final DiscoveryListener mDiscoverListener = new DiscoveryListener() { - - @Override - public void onDiscovery(final List devices) { - mLogger.info("Discovered devices: " + devices.size()); - if (mDeviceAdapter == null) { - return; - } - if (getActivity() == null) { - return; - } - getActivity().runOnUiThread(() -> { - mDeviceAdapter.clear(); - mDeviceAdapter.addAll(createDeviceContainers()); - mDeviceAdapter.notifyDataSetChanged(); - addFooterView(); - }); - } - - }; - - /** - * Create a list of device. - * - * @return list of device - */ - private List createDeviceContainers() { + private List createDeviceContainerList() { List containers = new ArrayList<>(); - List devices = getManager().getDeviceList(); - for (UVCDevice device : devices) { - containers.add(createContainer(device, device.isInitialized())); + if (mUVCDeviceService != null) { + for (DConnectService service : mUVCDeviceService.getServiceProvider().getServiceList()) { + containers.add(new DeviceContainer(service)); + } } return containers; } - /** - * Look for a DeviceContainer with the given id. - * - * @param id id of device - * @return The DeviceContainer that has the given id or null - */ - private DeviceContainer findDeviceContainerById(final String id) { - int size = mDeviceAdapter.getCount(); - for (int i = 0; i < size; i++) { - DeviceContainer container = mDeviceAdapter.getItem(i); - if (container.getId().equalsIgnoreCase(id)) { - return container; - } + public void onItemClick(AdapterView parent, View view, int position, long id) { + DeviceContainer container = mDeviceAdapter.getItem(position); + if (container.isOnline()) { + gotoRecorderList(container.getId()); } - return null; } - /** - * Create a DeviceContainer from UVCDevice. - * - * @param device Instance of UVCDevice - * @param register Registration flag - * @return Instance of DeviceContainer - */ - private DeviceContainer createContainer(final UVCDevice device, final boolean register) { - DeviceContainer container = new DeviceContainer(); - container.setName(device.getName()); - container.setId(device.getId()); - container.setRegisterFlag(register); - return container; + private void gotoRecorderList(String serviceId) { + Bundle bundle = new Bundle(); + bundle.putString("service_id", serviceId); + findNavController(this).navigate(R.id.action_service_to_recorder, bundle); } - private class DeviceContainer { - private String mName; - private String mId; - private boolean mRegisterFlag; + public class DeviceContainer { + private final DConnectService mService; - public String getName() { - return mName; + DeviceContainer(DConnectService service) { + mService = service; } - public void setName(final String name) { - if (name == null) { - mName = getActivity().getResources().getString( - R.string.uvc_settings_default_name); - } else { - mName = name; - } + public String getName() { + return mService.getName(); } public String getId() { - return mId; + return mService.getId(); } - public void setId(final String id) { - mId = id; + public String getStatus() { + return mService.isOnline() ? getString(R.string.uvc_settings_online) : getString(R.string.uvc_settings_offline); } - public boolean isRegisterFlag() { - return mRegisterFlag; + public boolean isOnline() { + return mService.isOnline(); } - public void setRegisterFlag(boolean registerFlag) { - mRegisterFlag = registerFlag; + public int getBackgroundColor() { + return mService.isOnline() ? Color.WHITE : Color.GRAY; } } - private class DeviceAdapter extends ArrayAdapter { - private LayoutInflater mInflater; - - public DeviceAdapter(final Context context, final List objects) { + private static class DeviceAdapter extends ArrayAdapter { + DeviceAdapter(final Context context, final List objects) { super(context, 0, objects); - mInflater = (LayoutInflater) context.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); + } + + void setContainers(List containers) { + clear(); + addAll(containers); + notifyDataSetChanged(); } @Override public View getView(final int position, View convertView, ViewGroup parent) { + ItemUvcDeviceBinding binding; if (convertView == null) { - convertView = mInflater.inflate(R.layout.item_uvc_device, null); - } - - final DeviceContainer device = getItem(position); - - String name = device.getName(); - TextView nameView = convertView.findViewById(R.id.device_name); - nameView.setText(name); - - Button btn = convertView.findViewById(R.id.btn_connect_device); - if (device.isRegisterFlag()) { - btn.setBackgroundResource(R.drawable.button_red); - btn.setText(R.string.uvc_settings_disconnect); + LayoutInflater inflater = LayoutInflater.from(getContext()); + binding = DataBindingUtil.inflate(inflater, R.layout.item_uvc_device, parent, false); + convertView = binding.getRoot(); + convertView.setTag(binding); } else { - btn.setBackgroundResource(R.drawable.button_blue); - btn.setText(R.string.uvc_settings_connect); + binding = (ItemUvcDeviceBinding) convertView.getTag(); } - btn.setOnClickListener((v) -> { - if (device.isRegisterFlag()) { - disconnectDevice(device); - } else { - connectDevice(device); - } - }); - + binding.setDeviceContainer(getItem(position)); return convertView; } } diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDevicePluginBindFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDevicePluginBindFragment.java new file mode 100644 index 0000000000..00342dc3f6 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDevicePluginBindFragment.java @@ -0,0 +1,168 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; + +import androidx.appcompat.app.ActionBar; +import androidx.fragment.app.Fragment; + +import org.deviceconnect.android.deviceplugin.uvc.UVCDeviceService; +import org.deviceconnect.android.deviceplugin.uvc.activity.UVCDevicePluginBindActivity; +import org.deviceconnect.android.deviceplugin.uvc.activity.UVCSettingsActivity; +import org.deviceconnect.android.deviceplugin.uvc.service.UVCService; + +import static androidx.navigation.fragment.NavHostFragment.findNavController; + +public class UVCDevicePluginBindFragment extends Fragment implements UVCDevicePluginBindActivity.OnUVCDevicePluginListener { + /** + * UI 操作用の Handler. + */ + private final Handler mUIHandler = new Handler(Looper.getMainLooper()); + + @Override + public void onResume() { + super.onResume(); + if (isBound()) { + // すでに接続されている場合には即座に呼び出す + onBindService(); + } + } + + @Override + public void onBindService() { + } + + @Override + public void onUnbindService() { + } + + @Override + public void onUvcConnected(UVCService service) { + } + + @Override + public void onUvcDisconnected(UVCService service) { + } + + /** + * 前の画面に戻ります. + * + * 前の画面がない場合には Activity を終了します。 + */ + public void popBackFragment() { + int entryCount = getParentFragmentManager().getBackStackEntryCount(); + if (entryCount == 0) { + getActivity().finish(); + } else { + findNavController(this).popBackStack(); + } + } + + /** + * ActionBar にタイトルを設定します. + * + * @param title タイトル + */ + public void setTitle(String title) { + Activity activity = getActivity(); + if (activity instanceof UVCDevicePluginBindActivity) { + ActionBar actionBar = ((UVCDevicePluginBindActivity) activity).getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(title); + } + } + } + + /** + * UVCDeviceService との接続状態を確認します. + * + * @return 接続中の場合はtrue、それ以外はfalse + */ + public boolean isBound() { + Activity activity = getActivity(); + if (activity instanceof UVCDevicePluginBindActivity) { + return ((UVCDevicePluginBindActivity) activity).isBound(); + } + return false; + } + + /** + * 接続されている UVCDeviceService のインスタンスを取得します. + *

+ * 接続されていない場合には null を返却します。 + * + * @return UVCDeviceService のインスタンス + */ + public UVCDeviceService getUVCDeviceService() { + Activity activity = getActivity(); + if (activity instanceof UVCDevicePluginBindActivity) { + return ((UVCDevicePluginBindActivity) activity).getUVCDeviceService(); + } + return null; + } + + /** + * 接続されている UVC サービスを取得します. + * + * 接続されていない場合には null を返却します。 + * + * @return UVCService のインスタンス + */ + public UVCService getUVCService() { + UVCDeviceService deviceService = getUVCDeviceService(); + if (deviceService != null) { + return deviceService.getActiveUVCService(); + } + return null; + } + + /** + * 画面の向き設定を取得します. + * + * @return 画面の向き設定 + */ + public int getDisplayOrientation() { + Activity activity = getActivity(); + if (activity != null) { + return activity.getRequestedOrientation(); + } + return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + } + + /** + * トーストを表示します. + * + * @param resId リソースID + */ + public void showToast(int resId) { + runOnUiThread(() -> { + Context context = getContext(); + if (context != null) { + Toast.makeText(context, resId, Toast.LENGTH_SHORT).show(); + } + }); + } + + /** + * UI スレッドで Runnable を実行します. + * + * @param run 実行する Runnable + */ + public void runOnUiThread(Runnable run) { + mUIHandler.post(run); + } + + /** + * Runnable を指定された delay の分だけ後に実行します. + * + * @param run 実行する Runnable + * @param delay 遅延する時間(ミリ秒) + */ + public void postDelay(Runnable run, long delay) { + mUIHandler.postDelayed(run, delay); + } +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDevicePluginBindPreferenceFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDevicePluginBindPreferenceFragment.java new file mode 100644 index 0000000000..4e746c533e --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCDevicePluginBindPreferenceFragment.java @@ -0,0 +1,150 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.preference.EditTextPreference; +import androidx.preference.PreferenceFragmentCompat; + +import org.deviceconnect.android.deviceplugin.uvc.UVCDeviceService; +import org.deviceconnect.android.deviceplugin.uvc.activity.UVCDevicePluginBindActivity; +import org.deviceconnect.android.deviceplugin.uvc.activity.UVCSettingsActivity; +import org.deviceconnect.android.deviceplugin.uvc.service.UVCService; + +import static androidx.navigation.fragment.NavHostFragment.findNavController; + +public abstract class UVCDevicePluginBindPreferenceFragment extends PreferenceFragmentCompat implements UVCDevicePluginBindActivity.OnUVCDevicePluginListener { + + private final Handler mUIHandler = new Handler(Looper.getMainLooper()); + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + if (view != null) { + view.setBackgroundColor(getResources().getColor(android.R.color.white)); + } + return view; + } + + @Override + public void onResume() { + super.onResume(); + + if (isBound()) { + onBindService(); + } + } + + @Override + public void onBindService() { + } + + @Override + public void onUnbindService() { + } + + @Override + public void onUvcConnected(UVCService service) { + } + + @Override + public void onUvcDisconnected(UVCService service) { + } + + /** + * 前の画面に戻ります. + * + * 前の画面がない場合には Activity を終了します。 + */ + public void popBackFragment() { + int entryCount = getParentFragmentManager().getBackStackEntryCount(); + if (entryCount == 0) { + getActivity().finish(); + } else { + findNavController(this).popBackStack(); + } + } + + /** + * ActionBar にタイトルを設定します. + * + * @param title タイトル + */ + public void setTitle(String title) { + Activity activity = getActivity(); + if (activity instanceof UVCDevicePluginBindActivity) { + ActionBar actionBar = ((UVCDevicePluginBindActivity) activity).getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(title); + } + } + } + + /** + * UVCDeviceService との接続を確認します. + * + * @return 接続されている場合はtrue、それ以外はfalse + */ + public boolean isBound() { + Activity activity = getActivity(); + if (activity instanceof UVCDevicePluginBindActivity) { + return ((UVCDevicePluginBindActivity) activity).isBound(); + } + return false; + } + + /** + * 接続されている UVCDeviceService のインスタンスを取得します. + *

+ * 接続されていない場合には null を返却します。 + * + * @return UVCDeviceService のインスタンス + */ + public UVCDeviceService getUVCDeviceService() { + Activity activity = getActivity(); + if (activity instanceof UVCDevicePluginBindActivity) { + return ((UVCDevicePluginBindActivity) activity).getUVCDeviceService(); + } + return null; + } + + /** + * 指定されたキーに対応する入力フォームを数値のみ入力可能に設定します. + * + * @param key キー + */ + public void setInputTypeNumber(String key) { + EditTextPreference editTextPreference = findPreference(key); + if (editTextPreference != null) { + editTextPreference.setOnBindEditTextListener((editText) -> + editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED)); + } + } + + /** + * トーストを表示します. + * + * @param resId リソースID + */ + public void showToast(int resId) { + runOnUiThread(() -> Toast.makeText(getContext(), resId, Toast.LENGTH_SHORT).show()); + } + + /** + * UI スレッドで Runnable を実行します. + * + * @param run 実行する Runnable + */ + public void runOnUiThread(Runnable run) { + mUIHandler.post(run); + } +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCPermissionConfirmationFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCPermissionConfirmationFragment.java new file mode 100644 index 0000000000..cc249b3cb7 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCPermissionConfirmationFragment.java @@ -0,0 +1,17 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import org.deviceconnect.android.deviceplugin.uvc.R; + +import static androidx.navigation.fragment.NavHostFragment.findNavController; + +public class UVCPermissionConfirmationFragment extends PermissionConfirmationFragment { + @Override + public void onNextFragment() { + findNavController(this).navigate(R.id.action_permission_to_plugin); + } + + @Override + public void onPermissionDeny() { + findNavController(this).navigate(R.id.action_permission_error_dialog); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCPluginSettingsFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCPluginSettingsFragment.java new file mode 100644 index 0000000000..0ec5088751 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCPluginSettingsFragment.java @@ -0,0 +1,50 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.app.Activity; +import android.os.Bundle; + +import androidx.preference.Preference; +import androidx.preference.SwitchPreferenceCompat; + +import org.deviceconnect.android.deviceplugin.uvc.R; +import org.deviceconnect.android.deviceplugin.uvc.UVCDeviceService; +import org.deviceconnect.android.deviceplugin.uvc.activity.UVCSettingsActivity; + +import static androidx.navigation.fragment.NavHostFragment.findNavController; + +public class UVCPluginSettingsFragment extends UVCDevicePluginBindPreferenceFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.settings_uvc_plugin, rootKey); + } + + @Override + public void onBindService() { + UVCDeviceService deviceService = getUVCDeviceService(); + if (deviceService != null) { + setAuth(deviceService); + } + setTitle(getString(R.string.uvc_settings_uvc_title)); + } + + @Override + public boolean onPreferenceTreeClick(final Preference preference) { + if ("uvc_settings_service_list".equals(preference.getKey())) { + findNavController(this).navigate(R.id.action_plugin_to_service); + } else if ("uvc_settings_service_instruction".equals(preference.getKey())) { + findNavController(this).navigate(R.id.action_plugin_to_instruction); + } + return super.onPreferenceTreeClick(preference); + } + + private void setAuth(UVCDeviceService deviceService) { + SwitchPreferenceCompat pref = findPreference("uvc_settings_auth"); + if (pref != null) { + pref.setChecked(deviceService.isUseLocalOAuth()); + pref.setOnPreferenceChangeListener((preference, newValue) -> { + deviceService.setUseLocalOAuth((Boolean) newValue); + return true; + }); + } + } +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCRecorderListFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCRecorderListFragment.java new file mode 100644 index 0000000000..680f42933f --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCRecorderListFragment.java @@ -0,0 +1,176 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; + +import org.deviceconnect.android.deviceplugin.uvc.R; +import org.deviceconnect.android.deviceplugin.uvc.UVCDeviceService; +import org.deviceconnect.android.deviceplugin.uvc.databinding.FragmentUvcRecorderListBinding; +import org.deviceconnect.android.deviceplugin.uvc.databinding.ItemUvcRecorderBinding; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.uvc.UvcRecorder; +import org.deviceconnect.android.deviceplugin.uvc.service.UVCService; +import org.deviceconnect.android.service.DConnectService; + +import java.util.ArrayList; +import java.util.List; + +import static androidx.navigation.fragment.NavHostFragment.findNavController; + +public class UVCRecorderListFragment extends UVCDevicePluginBindFragment { + private RecorderAdapter mAdapter; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + FragmentUvcRecorderListBinding binding = DataBindingUtil.inflate(inflater, + R.layout.fragment_uvc_recorder_list, container, false); + binding.setPresenter(this); + + mAdapter = new RecorderAdapter(getContext(), new ArrayList<>()); + + View rootView = binding.getRoot(); + ListView listView = rootView.findViewById(R.id.recorder_list_view); + listView.setAdapter(mAdapter); + listView.setItemsCanFocus(true); + return rootView; + } + + @Override + public void onBindService() { + UVCDeviceService deviceService = getUVCDeviceService(); + if (deviceService != null) { + String serviceId = getServiceId(); + if (serviceId != null) { + UVCService service = deviceService.findUVCServiceById(serviceId); + if (service != null) { + setTitle(service.getName()); + } + } + } + mAdapter.setContainers(createRecorderContainerList()); + + if (!isOnlineUVCService()) { + // UVC が接続されていない場合には前の画面に戻ります + popBackFragment(); + } + } + + @Override + public void onUvcDisconnected(UVCService service) { + popBackFragment(); + } + + public boolean isOnlineUVCService() { + UVCService service = getUVCService(); + if (service != null) { + return service.isOnline(); + } + return false; + } + + private String getServiceId() { + Bundle args = getArguments(); + if (args != null) { + return args.getString("service_id"); + } + return null; + } + + public void onItemClick(AdapterView parent, View view, int position, long id) { + gotoRecorderSettings(mAdapter.getItem(position)); + } + + private void gotoRecorderSettings(RecorderContainer container) { + Bundle bundle = new Bundle(); + bundle.putString("service_id", container.getServiceId()); + bundle.putString("recorder_id", container.getId()); + bundle.putString("settings_name", container.getSettingsName()); + findNavController(this).navigate(R.id.action_recorder_to_main, bundle); + } + + private List createRecorderContainerList() { + List containers = new ArrayList<>(); + + UVCDeviceService deviceService = getUVCDeviceService(); + if (deviceService != null) { + String serviceId = getServiceId(); + if (serviceId != null) { + UVCService service = deviceService.findUVCServiceById(serviceId); + if (service != null) { + for (MediaRecorder recorder : service.getUvcRecorderList()) { + if (recorder instanceof UvcRecorder) { + containers.add(new RecorderContainer(service, (UvcRecorder) recorder)); + } + } + } + } + } + + return containers; + } + + public static class RecorderContainer { + private final DConnectService mService; + private final MediaRecorder mRecorder; + private final String mSettingsName; + + RecorderContainer(DConnectService service, UvcRecorder recorder) { + mService = service; + mRecorder = recorder; + mSettingsName = recorder.getSettingsName(); + } + + public String getName() { + return mRecorder.getName(); + } + + public String getId() { + return mRecorder.getId(); + } + + public String getSettingsName() { + return mSettingsName; + } + + public String getServiceId() { + return mService.getId(); + } + } + + private static class RecorderAdapter extends ArrayAdapter { + RecorderAdapter(final Context context, final List objects) { + super(context, 0, objects); + } + + void setContainers(List containers) { + clear(); + addAll(containers); + notifyDataSetChanged(); + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + ItemUvcRecorderBinding binding; + if (convertView == null) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + binding = DataBindingUtil.inflate(inflater, R.layout.item_uvc_recorder, parent, false); + convertView = binding.getRoot(); + convertView.setTag(binding); + } else { + binding = (ItemUvcRecorderBinding) convertView.getTag(); + } + binding.setContainer(getItem(position)); + return convertView; + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsBaseFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsBaseFragment.java new file mode 100644 index 0000000000..85dbd5e56d --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsBaseFragment.java @@ -0,0 +1,118 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import org.deviceconnect.android.deviceplugin.uvc.UVCDeviceService; +import org.deviceconnect.android.deviceplugin.uvc.activity.UVCDevicePluginBindActivity; +import org.deviceconnect.android.deviceplugin.uvc.activity.UVCSettingsActivity; +import org.deviceconnect.android.deviceplugin.uvc.recorder.uvc.UvcRecorder; +import org.deviceconnect.android.deviceplugin.uvc.service.UVCService; + +public abstract class UVCSettingsBaseFragment extends UVCDevicePluginBindPreferenceFragment { + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + if (view != null) { + view.setBackgroundColor(getResources().getColor(android.R.color.white)); + } + return view; + } + + @Override + public void onBindService() { + String title = createTitle(); + if (title != null) { + setTitle(title); + } + } + + @Override + public void onUvcDisconnected(UVCService service) { + popBackFragment(); + } + + private String createTitle() { + UVCService service = getUVCService(); + if (service != null) { + String title = service.getName(); + UvcRecorder recorder = getRecorder(); + if (recorder != null) { + title += " - " + recorder.getName(); + } + return title; + } + return null; + } + + public UVCService getUVCService() { + UVCDeviceService deviceService = getUVCDeviceService(); + if (deviceService != null) { + return deviceService.findUVCServiceById(getServiceId()); + } + return null; + } + + public boolean isOnlineUVCService() { + UVCService service = getUVCService(); + if (service != null) { + return service.isOnline(); + } + return false; + } + + public UvcRecorder getRecorder() { + UVCService service = getUVCService(); + if (service != null) { + return service.findUvcRecorderById(getRecorderId()); + } + return null; + } + + public String getServiceId() { + return getArgs("service_id"); + } + + public String getRecorderId() { + return getArgs("recorder_id"); + } + + public String getSettingsName() { + return getArgs("settings_name"); + } + + private String getArgs(String key) { + String serviceId = getBundleArgs(key); + if (serviceId == null) { + serviceId = getIntentArgs(key); + } + return serviceId; + } + + private String getBundleArgs(String key) { + Bundle args = getArguments(); + if (args != null) { + return args.getString(key); + } + return null; + } + + private String getIntentArgs(String key) { + Activity a = getActivity(); + if (a != null) { + Intent args = a.getIntent(); + if (args != null) { + return args.getStringExtra(key); + } + } + return null; + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsBroadcastFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsBroadcastFragment.java new file mode 100644 index 0000000000..ddfbdc3eb7 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsBroadcastFragment.java @@ -0,0 +1,13 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.os.Bundle; + +import org.deviceconnect.android.deviceplugin.uvc.R; + +public class UVCSettingsBroadcastFragment extends UVCSettingsBaseFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + getPreferenceManager().setSharedPreferencesName(getSettingsName()); + setPreferencesFromResource(R.xml.settings_uvc_recorder_broadcast, rootKey); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsMainFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsMainFragment.java new file mode 100644 index 0000000000..f368fb233e --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsMainFragment.java @@ -0,0 +1,57 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.os.Bundle; + +import androidx.preference.Preference; + +import org.deviceconnect.android.deviceplugin.uvc.R; + +import static androidx.navigation.fragment.NavHostFragment.findNavController; + +public class UVCSettingsMainFragment extends UVCSettingsBaseFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.settings_uvc_recorder_main, rootKey); + } + + @Override + public void onBindService() { + super.onBindService(); + + if (!isOnlineUVCService()) { + // UVC が接続されていない場合には前の画面に戻ります + popBackFragment(); + } + } + + @Override + public boolean onPreferenceTreeClick(final Preference preference) { + Bundle arguments = createArguments(); + if (arguments != null) { + if ("recorder_settings_video".equals(preference.getKey())) { + findNavController(this).navigate(R.id.action_main_to_video, arguments); + } else if ("recorder_settings_srt".equals(preference.getKey())) { + findNavController(this).navigate(R.id.action_main_to_srt, arguments); + } else if ("recorder_settings_broadcast".equals(preference.getKey())) { + findNavController(this).navigate(R.id.action_main_to_broadcast, arguments); + } else if ("recorder_settings_port".equals(preference.getKey())) { + findNavController(this).navigate(R.id.action_main_to_port, arguments); + } + } + return super.onPreferenceTreeClick(preference); + } + + private Bundle createArguments() { + String serviceId = getServiceId(); + String recorderId = getRecorderId(); + String settingsName = getSettingsName(); + if (serviceId != null && recorderId != null && settingsName != null) { + Bundle bundle = new Bundle(); + bundle.putString("service_id", serviceId); + bundle.putString("recorder_id", recorderId); + bundle.putString("settings_name", settingsName); + return bundle; + } + return null; + } +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsParameterFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsParameterFragment.java new file mode 100644 index 0000000000..84c9e8fb3a --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsParameterFragment.java @@ -0,0 +1,66 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.content.SharedPreferences; + +import androidx.fragment.app.DialogFragment; +import androidx.preference.Preference; + +import org.deviceconnect.android.deviceplugin.uvc.fragment.preference.SeekBarDialogFragment; +import org.deviceconnect.android.deviceplugin.uvc.fragment.preference.SeekBarDialogPreference; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; + +public abstract class UVCSettingsParameterFragment extends UVCSettingsBaseFragment { + /** + * カスタム Preference を判別するためのタグ. + */ + private static final String DIALOG_FRAGMENT_TAG = "CustomPreference"; + + /** + * 設定が変更されたことを保持するフラグ. + */ + private boolean mChangedValue; + + /** + * 設定が変更されてことを受信するリスナー. + */ + private final SharedPreferences.OnSharedPreferenceChangeListener mListener = + (sharedPreferences, key) -> mChangedValue = true; + + @Override + public void onDisplayPreferenceDialog(Preference preference) { + if (getFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) { + return; + } + + if (preference instanceof SeekBarDialogPreference) { + DialogFragment f = SeekBarDialogFragment.newInstance(preference.getKey()); + f.setTargetFragment(this, 0); + f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); + } else { + super.onDisplayPreferenceDialog(preference); + } + } + + @Override + public void onPause() { + getPreferenceManager().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(mListener); + + MediaRecorder recorder = getRecorder(); + + // 設定が変更されていた場合には、レコーダに通知を行う + if (mChangedValue && recorder != null) { + recorder.onConfigChange(); + } + + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + mChangedValue = false; + getPreferenceManager().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(mListener); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsPortFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsPortFragment.java new file mode 100644 index 0000000000..054d60227f --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsPortFragment.java @@ -0,0 +1,22 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.os.Bundle; + +import org.deviceconnect.android.deviceplugin.uvc.R; + +public class UVCSettingsPortFragment extends UVCSettingsBaseFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + getPreferenceManager().setSharedPreferencesName(getSettingsName()); + setPreferencesFromResource(R.xml.settings_uvc_recorder_port, rootKey); + } + + @Override + public void onBindService() { + super.onBindService(); + + setInputTypeNumber("mjpeg_port"); + setInputTypeNumber("rtsp_port"); + setInputTypeNumber("srt_port"); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsSRTFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsSRTFragment.java new file mode 100644 index 0000000000..a313efc1d9 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsSRTFragment.java @@ -0,0 +1,43 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.preference.EditTextPreference; +import androidx.preference.Preference; + +import org.deviceconnect.android.deviceplugin.uvc.R; + +public class UVCSettingsSRTFragment extends UVCSettingsBaseFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + getPreferenceManager().setSharedPreferencesName(getSettingsName()); + setPreferencesFromResource(R.xml.settings_uvc_recorder_srt, rootKey); + + setSummaryOptionAuto(getString(R.string.pref_key_settings_srt_inputbw)); + setSummaryOptionAuto(getString(R.string.pref_key_settings_srt_oheadbw)); + + setInputTypeNumber(getString(R.string.pref_key_settings_srt_peerlatency)); + setInputTypeNumber(getString(R.string.pref_key_settings_srt_lossmaxttl)); + setInputTypeNumber(getString(R.string.pref_key_settings_srt_oheadbw)); + setInputTypeNumber(getString(R.string.pref_key_settings_srt_inputbw)); + setInputTypeNumber(getString(R.string.pref_key_settings_srt_conntimeo)); + setInputTypeNumber(getString(R.string.pref_key_settings_srt_peeridletimeo)); + setInputTypeNumber(getString(R.string.pref_key_settings_srt_packetfilter)); + } + + private void setSummaryOptionAuto(String name) { + EditTextPreference inputBwPref = findPreference(name); + if (inputBwPref != null) { + inputBwPref.setSummaryProvider(summaryOptionAuto); + } + } + + private final Preference.SummaryProvider summaryOptionAuto = (preference) -> { + String value = preference.getText(); + if (TextUtils.isEmpty(value)) { + return getString(R.string.uvc_setting_srt_option_auto); + } + return value; + }; +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsVideoFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsVideoFragment.java new file mode 100644 index 0000000000..0357c157b0 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/UVCSettingsVideoFragment.java @@ -0,0 +1,480 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment; + +import android.os.Bundle; +import android.util.Log; +import android.util.Size; + +import androidx.annotation.NonNull; +import androidx.preference.EditTextPreference; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import org.deviceconnect.android.deviceplugin.uvc.R; +import org.deviceconnect.android.deviceplugin.uvc.fragment.preference.SeekBarDialogPreference; +import org.deviceconnect.android.deviceplugin.uvc.profile.utils.H264Level; +import org.deviceconnect.android.deviceplugin.uvc.profile.utils.H264Profile; +import org.deviceconnect.android.deviceplugin.uvc.profile.utils.H265Level; +import org.deviceconnect.android.deviceplugin.uvc.profile.utils.H265Profile; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; +import org.deviceconnect.android.deviceplugin.uvc.util.CapabilityUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class UVCSettingsVideoFragment extends UVCSettingsParameterFragment { + private MediaRecorder mMediaRecorder; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + getPreferenceManager().setSharedPreferencesName(getSettingsName()); + setPreferencesFromResource(R.xml.settings_uvc_recorder_video, rootKey); + } + + @Override + public void onBindService() { + super.onBindService(); + + mMediaRecorder = getRecorder(); + + if (mMediaRecorder == null) { + return; + } + + MediaRecorder.Settings settings = mMediaRecorder.getSettings(); + + setPictureSizePreference(settings); + setPreviewSizePreference(settings); + setPreviewVideoEncoderPreference(settings); + setPreviewProfileLevelPreference(settings); + setPreviewJpegQuality(); + + setInputTypeNumber("preview_framerate"); + setInputTypeNumber("preview_bitrate"); + setInputTypeNumber("preview_i_frame_interval"); + setInputTypeNumber("preview_intra_refresh"); + + setPreviewClipPreference("preview_clip_left"); + setPreviewClipPreference("preview_clip_top"); + setPreviewClipPreference("preview_clip_right"); + setPreviewClipPreference("preview_clip_bottom"); + setPreviewCutOutReset(); + } + + /** + * 静止画の解像度の Preference を作成します. + * + * @param settings レコーダの設定 + */ + private void setPictureSizePreference(MediaRecorder.Settings settings) { + ListPreference pref = findPreference("camera_picture_size"); + if (pref != null) { + List pictureSizes = getSupportedPictureSizes(settings); + if (!pictureSizes.isEmpty()) { + List entryValues = new ArrayList<>(); + for (Size preview : pictureSizes) { + entryValues.add(getValueFromSize(preview)); + } + pref.setEntries(entryValues.toArray(new String[0])); + pref.setEntryValues(entryValues.toArray(new String[0])); + pref.setOnPreferenceChangeListener(mOnPreferenceChangeListener); + + Size pictureSize = settings.getPictureSize(); + if (pictureSize != null) { + pref.setValue(getValueFromSize(pictureSize)); + } + pref.setVisible(true); + } else { + pref.setEnabled(false); + } + } + } + + /** + * プレビューの解像度 Preference を作成します. + * + * @param settings レコーダの設定 + */ + private void setPreviewSizePreference(MediaRecorder.Settings settings) { + ListPreference pref = findPreference("camera_preview_size"); + if (pref != null) { + List previewSizes = getSupportedPreviewSizes(settings); + if (!previewSizes.isEmpty()) { + List entryValues = new ArrayList<>(); + for (Size preview : previewSizes) { + entryValues.add(getValueFromSize(preview)); + } + + pref.setEntries(entryValues.toArray(new String[0])); + pref.setEntryValues(entryValues.toArray(new String[0])); + pref.setOnPreferenceChangeListener(mOnPreferenceChangeListener); + + Size previewSize = settings.getPreviewSize(); + if (previewSize != null) { + pref.setValue(getValueFromSize(previewSize)); + } + pref.setVisible(true); + } else { + pref.setEnabled(false); + } + } + } + + /** + * エンコーダの設定を行います. + * + * @param settings レコーダ設定 + */ + private void setPreviewVideoEncoderPreference(MediaRecorder.Settings settings) { + ListPreference pref = findPreference("preview_encoder"); + if (pref != null) { + List list = settings.getSupportedVideoEncoders(); + if (!list.isEmpty()) { + List entryValues = new ArrayList<>(list); + pref.setEntries(entryValues.toArray(new String[0])); + pref.setEntryValues(entryValues.toArray(new String[0])); + pref.setOnPreferenceChangeListener(mOnPreferenceChangeListener); + pref.setVisible(true); + } else { + pref.setEnabled(false); + } + } + } + + /** + * エンコーダのプロファイルとレベルを設定します. + * + * @param settings レコーダ設定 + */ + private void setPreviewProfileLevelPreference(MediaRecorder.Settings settings) { + setPreviewProfileLevelPreference(settings, settings.getPreviewEncoderName(), false); + } + + /** + * エンコーダのプロファイルとレベルを設定します. + * + * @param settings レコーダ設定 + * @param encoderName エンコーダ + * @param reset リセットフラグ + */ + private void setPreviewProfileLevelPreference(MediaRecorder.Settings settings, MediaRecorder.VideoEncoderName encoderName, boolean reset) { + ListPreference pref = findPreference("preview_profile_level"); + if (pref != null) { + List list = CapabilityUtil.getSupportedProfileLevel(encoderName.getMimeType()); + if (!list.isEmpty()) { + List entryValues = new ArrayList<>(); + entryValues.add("none"); + + for (MediaRecorder.ProfileLevel pl : list) { + String value = getProfileLevel(encoderName, pl); + if (value != null) { + entryValues.add(value); + } + } + + pref.setEntries(entryValues.toArray(new String[0])); + pref.setEntryValues(entryValues.toArray(new String[0])); + pref.setOnPreferenceChangeListener(mOnPreferenceChangeListener); + + if (reset) { + pref.setValue("none"); + } else { + MediaRecorder.ProfileLevel pl = settings.getProfileLevel(); + if (pl != null) { + pref.setValue(getProfileLevel(encoderName, pl)); + } + } + + pref.setVisible(true); + } else { + pref.setEnabled(false); + } + } + } + + /** + * JPEG クオリティを設定します. + */ + private void setPreviewJpegQuality() { + SeekBarDialogPreference pref = findPreference("preview_jpeg_quality"); + if (pref != null) { + pref.setMinValue(0); + pref.setMaxValue(100); + pref.setEnabled(true); + } + } + + /** + * 切り抜き範囲の設定にリスナーを設定します. + * + * @param key キー + */ + public void setPreviewClipPreference(String key) { + EditTextPreference pref = findPreference(key); + if (pref != null) { + pref.setOnPreferenceChangeListener(mOnPreferenceChangeListener); + setInputTypeNumber(key); + } + } + + /** + * EditText を未設定に戻します. + * + * @param key キー + */ + private void resetEditText(String key) { + EditTextPreference pref = findPreference(key); + if (pref != null) { + pref.setText(null); + } + } + + /** + * 切り抜き範囲のリセットボタンのリスナーを設定します. + */ + private void setPreviewCutOutReset() { + PreferenceScreen pref = findPreference("preview_clip_reset"); + if (pref != null) { + pref.setOnPreferenceClickListener(preference -> { + resetEditText("preview_clip_left"); + resetEditText("preview_clip_top"); + resetEditText("preview_clip_right"); + resetEditText("preview_clip_bottom"); + mMediaRecorder.getSettings().setDrawingRange(null); + return false; + }); + } + } + /** + * サイズの小さい方からソートを行うための比較演算子. + */ + private static final Comparator SIZE_COMPARATOR = (lhs, rhs) -> { + // We cast here to ensure the multiplications won't overflow + return Long.signum((long) lhs.getWidth() * lhs.getHeight() - + (long) rhs.getWidth() * rhs.getHeight()); + }; + + /** + * カメラID に対応したカメラデバイスがサポートしている写真サイズのリストを取得します. + * + * @param settings レコーダ + * @return サポートしているプレビューサイズのリスト + */ + @NonNull + private static List getSupportedPictureSizes(MediaRecorder.Settings settings) { + List previewSizes = new ArrayList<>(); + if (settings != null) { + previewSizes.addAll(settings.getSupportedPictureSizes()); + Collections.sort(previewSizes, SIZE_COMPARATOR); + } + return previewSizes; + } + + /** + * カメラID に対応したカメラデバイスがサポートしているプレビューサイズのリストを取得します. + * + * @param settings レコーダ + * @return サポートしているプレビューサイズのリスト + */ + @NonNull + private static List getSupportedPreviewSizes(MediaRecorder.Settings settings) { + List previewSizes = new ArrayList<>(); + if (settings != null) { + previewSizes.addAll(settings.getSupportedPreviewSizes()); + Collections.sort(previewSizes, SIZE_COMPARATOR); + } + return previewSizes; + } + + /** + * プレビューのサイズを文字列に変換します. + * + * @param previewSize プレビューサイズ + * @return 文字列 + */ + private String getValueFromSize(Size previewSize) { + return previewSize.getWidth() + " x " + previewSize.getHeight(); + } + + /** + * 文字列を Size に変換します. + * + * Size に変換できなかった場合には null を返却します。 + * + * @param value 文字列のサイズ + * @return サイズ + */ + private Size getSizeFromValue(String value) { + String[] t = value.split("x"); + if (t.length == 2) { + try { + int w = Integer.parseInt(t[0].trim()); + int h = Integer.parseInt(t[1].trim()); + return new Size(w, h); + } catch (Exception e) { + return null; + } + } + return null; + } + + /** + * プロファイルとレベルを文字列に変換します. + * + * @param encoderName エンコーダ + * @param pl プロファイルとレベル + * @return 文字列 + */ + private String getProfileLevel(MediaRecorder.VideoEncoderName encoderName, MediaRecorder.ProfileLevel pl) { + switch (encoderName) { + case H264: { + H264Profile p = H264Profile.valueOf(pl.getProfile()); + H264Level l = H264Level.valueOf(pl.getLevel()); + if (p != null && l != null) { + return p.getName() + " - " + l.getName(); + } + } + case H265: { + H265Profile p = H265Profile.valueOf(pl.getProfile()); + H265Level l = H265Level.valueOf(pl.getLevel()); + if (p != null && l != null) { + return p.getName() + " - " + l.getName(); + } + } + } + return null; + } + + /** + * 文字列をプロファイルとレベルに変換します. + * + * プロファイルとレベルに変換できなかった場合には、null を返却します。 + * + * @param encoderName エンコーダ + * @param value 変換する文字列 + * @return プロファイルとレベル + */ + private MediaRecorder.ProfileLevel getProfileLevel(MediaRecorder.VideoEncoderName encoderName, String value) { + String[] t = value.split("-"); + if (t.length == 2) { + try { + String profile = t[0].trim(); + String level = t[1].trim(); + switch (encoderName) { + case H264: { + H264Profile p = H264Profile.nameOf(profile); + H264Level l = H264Level.nameOf(level); + if (p != null && l != null) { + return new MediaRecorder.ProfileLevel(p.getValue(), l.getValue()); + } + } + case H265: { + H265Profile p = H265Profile.nameOf(profile); + H265Level l = H265Level.nameOf(level); + if (p != null && l != null) { + return new MediaRecorder.ProfileLevel(p.getValue(), l.getValue()); + } + } + } + } catch (Exception e) { + return null; + } + } + return null; + } + + /** + * 切り抜き範囲の値を取得します. + * + * 未設定の場合には null を返却します。 + * + * @param key キー + * @return 切り抜き範囲 + */ + private Integer getDrawingRange(String key) { + EditTextPreference pref = findPreference(key); + if (pref != null) { + try { + return Integer.parseInt(pref.getText()); + } catch (NumberFormatException e) { + // ignore. + } + } + return null; + } + + /** + * 設定が変更された時に呼び出されるリスナー. + */ + private final Preference.OnPreferenceChangeListener mOnPreferenceChangeListener = (preference, newValue) -> { + if (mMediaRecorder == null) { + return false; + } + + MediaRecorder.Settings settings = mMediaRecorder.getSettings(); + + String key = preference.getKey(); + if ("camera_picture_size".equals(key)) { + Size size = getSizeFromValue((String) newValue); + if (size != null) { + settings.setPictureSize(size); + } + } else if ("camera_preview_size".equals(key)) { + Size size = getSizeFromValue((String) newValue); + if (size != null) { + settings.setPreviewSize(size); + } + } else if ("preview_encoder".equals(key)) { + // エンコーダが切り替えられたので、プロファイル・レベルは一旦削除しておく + settings.setProfileLevel(null); + MediaRecorder.VideoEncoderName encoderName = + MediaRecorder.VideoEncoderName.nameOf((String) newValue); + setPreviewProfileLevelPreference(settings, encoderName, true); + } else if ("preview_profile_level".equalsIgnoreCase(key)) { + settings.setProfileLevel(getProfileLevel(settings.getPreviewEncoderName(), (String) newValue)); + } else if ("preview_clip_left".equalsIgnoreCase(key)) { + try { + int clipLeft = Integer.parseInt((String) newValue); + Integer clipRight = getDrawingRange("preview_clip_right"); + if (clipRight != null && clipRight <= clipLeft) { + return false; + } + } catch (NumberFormatException e) { + return false; + } + } else if ("preview_clip_top".equalsIgnoreCase(key)) { + try { + int clipTop = Integer.parseInt((String) newValue); + Integer clipBottom = getDrawingRange("preview_clip_bottom"); + if (clipBottom != null && clipBottom <= clipTop) { + return false; + } + } catch (NumberFormatException e) { + return false; + } + } else if ("preview_clip_right".equalsIgnoreCase(key)) { + try { + int clipRight = Integer.parseInt((String) newValue); + Integer clipLeft = getDrawingRange("preview_clip_left"); + if (clipLeft != null && clipRight <= clipLeft) { + return false; + } + } catch (NumberFormatException e) { + return false; + } + } else if ("preview_clip_bottom".equalsIgnoreCase(key)) { + try { + int clipBottom = Integer.parseInt((String) newValue); + Integer clipTop = getDrawingRange("preview_clip_top"); + if (clipTop != null && clipBottom <= clipTop) { + return false; + } + } catch (NumberFormatException e) { + return false; + } + } + return true; + }; +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/dialog/ErrorDialogFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/dialog/ErrorDialogFragment.java deleted file mode 100644 index 93134e3a84..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/dialog/ErrorDialogFragment.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - ErrorDialogFragment - Copyright (c) 2015 NTT DOCOMO,INC. - Released under the MIT license - http://opensource.org/licenses/mit-license.php - */ -package org.deviceconnect.android.deviceplugin.uvc.fragment.dialog; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; - -import androidx.fragment.app.DialogFragment; - -import org.deviceconnect.android.deviceplugin.uvc.R; - -/** - * This fragment displays a dialog of error. - * @author NTT DOCOMO, INC. - */ -public class ErrorDialogFragment extends DialogFragment { - private static final String PARAM_TITLE = "title"; - private static final String PARAM_MESSAGE = "message"; - private AlertDialog mDialog; - private DialogInterface.OnDismissListener mListener; - - public static ErrorDialogFragment newInstance(final String title, final String message) { - ErrorDialogFragment instance = new ErrorDialogFragment(); - - Bundle arguments = new Bundle(); - arguments.putString(PARAM_TITLE, title); - arguments.putString(PARAM_MESSAGE, message); - - instance.setArguments(arguments); - - return instance; - } - - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - if (mDialog != null) { - return mDialog; - } - - String title = getArguments().getString(PARAM_TITLE); - String message = getArguments().getString(PARAM_MESSAGE); - - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(title); - builder.setMessage(message); - builder.setPositiveButton(R.string.uvc_settings_dialog_positive, - (dialog, which) -> { - dismiss(); - }); - mDialog = builder.create(); - return mDialog; - } - - @Override - public Dialog getDialog() { - return mDialog; - } - - @Override - public void onDestroy() { - super.onDestroy(); - mDialog = null; - } - - @Override - public void onDismiss(final DialogInterface dialog) { - super.onDismiss(dialog); - if (mListener != null) { - mListener.onDismiss(dialog); - } - } - - public void setOnDismissListener(final DialogInterface.OnDismissListener listener) { - mListener = listener; - } -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/dialog/PermissionErrorDialogFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/dialog/PermissionErrorDialogFragment.java new file mode 100644 index 0000000000..c298db7c18 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/dialog/PermissionErrorDialogFragment.java @@ -0,0 +1,27 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment.dialog; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; + +import androidx.fragment.app.DialogFragment; + +import org.deviceconnect.android.deviceplugin.uvc.R; + +public class PermissionErrorDialogFragment extends DialogFragment { + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); + builder.setTitle(R.string.uvc_error_permission_title); + builder.setMessage(R.string.uvc_error_permission_message); + builder.setPositiveButton(R.string.uvc_error_positive, (dialog, which) -> { + Activity a = getActivity(); + if (a != null) { + a.finish(); + } + }); + setCancelable(false); + return builder.create(); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/dialog/ProgressDialogFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/dialog/ProgressDialogFragment.java deleted file mode 100644 index c6ea17e4d5..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/dialog/ProgressDialogFragment.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - ProgressDialogFragment - Copyright (c) 2015 NTT DOCOMO,INC. - Released under the MIT license - http://opensource.org/licenses/mit-license.php - */ -package org.deviceconnect.android.deviceplugin.uvc.fragment.dialog; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; - -import androidx.fragment.app.DialogFragment; - -import org.deviceconnect.android.deviceplugin.uvc.R; - -/** - * This fragment displays a dialog of Progress. - * @author NTT DOCOMO, INC. - */ -public class ProgressDialogFragment extends DialogFragment { - private static final String PARAM_TITLE = "title"; - private static final String PARAM_MESSAGE = "message"; - public static ProgressDialogFragment newInstance(final String title, final String message) { - ProgressDialogFragment instance = new ProgressDialogFragment(); - - Bundle arguments = new Bundle(); - arguments.putString(PARAM_TITLE, title); - arguments.putString(PARAM_MESSAGE, message); - - instance.setArguments(arguments); - - return instance; - } - - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - String title = getArguments().getString(PARAM_TITLE); - String message = getArguments().getString(PARAM_MESSAGE); - - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - LayoutInflater inflater = getActivity().getLayoutInflater(); - View v = inflater.inflate(R.layout.dialog_progress, null); - TextView titleView = v.findViewById(R.id.title); - TextView messageView = v.findViewById(R.id.message); - titleView.setText(title); - messageView.setText(message); - builder.setView(v); - - return builder.create(); - } -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/preference/SeekBarDialogFragment.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/preference/SeekBarDialogFragment.java new file mode 100644 index 0000000000..142c0e9073 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/preference/SeekBarDialogFragment.java @@ -0,0 +1,74 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment.preference; + +import android.os.Bundle; +import android.view.View; +import android.widget.EditText; +import android.widget.SeekBar; + +import androidx.preference.EditTextPreference; +import androidx.preference.PreferenceDialogFragmentCompat; + +import org.deviceconnect.android.deviceplugin.uvc.R; + +public class SeekBarDialogFragment extends PreferenceDialogFragmentCompat { + private static final int MAX = 1000; + private EditText mValueEditText; + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + SeekBarDialogPreference pref = (SeekBarDialogPreference) getPreference(); + int min = pref.getMinValue(); + int max = pref.getMaxValue(); + long value = min; + try { + value = Long.parseLong(pref.getText()); + } catch (Exception e) { + // ignore. + } + + mValueEditText = view.findViewById(R.id.number_edit_text); + if (mValueEditText != null) { + mValueEditText.setText(String.valueOf(value)); + } + + SeekBar seekBar = view.findViewById(R.id.seekbar); + if (seekBar != null) { + seekBar.setMax(MAX); + seekBar.setProgress((int) (MAX * ((value - min) / (float) (max - min)))); + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + long value = min + (long) ((max - min) * (progress / (float) MAX)); + mValueEditText.setText(String.valueOf(value)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + } + } + + @Override + public void onDialogClosed(boolean positiveResult) { + EditTextPreference f = (EditTextPreference) getPreference(); + if (positiveResult) { + f.setText(mValueEditText.getText().toString()); + } + } + + public static SeekBarDialogFragment newInstance(String key) { + Bundle bundle = new Bundle(); + bundle.putString(ARG_KEY, key); + + SeekBarDialogFragment fragment = new SeekBarDialogFragment(); + fragment.setArguments(bundle); + return fragment; + } +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/preference/SeekBarDialogPreference.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/preference/SeekBarDialogPreference.java new file mode 100644 index 0000000000..f6f41d0557 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/fragment/preference/SeekBarDialogPreference.java @@ -0,0 +1,50 @@ +package org.deviceconnect.android.deviceplugin.uvc.fragment.preference; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.preference.EditTextPreference; + +import org.deviceconnect.android.deviceplugin.uvc.R; + +public class SeekBarDialogPreference extends EditTextPreference { + private int mMinValue; + private int mMaxValue; + + public SeekBarDialogPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public SeekBarDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SeekBarDialogPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SeekBarDialogPreference(Context context) { + super(context); + } + + @Override + public int getDialogLayoutResource() { + return R.layout.dialog_preference_seek_bar; + } + + public void setMinValue(int minValue) { + mMinValue = minValue; + } + + public int getMinValue() { + return mMinValue; + } + + public void setMaxValue(int maxValue) { + mMaxValue = maxValue; + } + + public int getMaxValue() { + return mMaxValue; + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/UVCMediaStreamRecordingProfile.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/UVCMediaStreamRecordingProfile.java index 2bc05f8e56..49e525d955 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/UVCMediaStreamRecordingProfile.java +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/UVCMediaStreamRecordingProfile.java @@ -6,19 +6,24 @@ */ package org.deviceconnect.android.deviceplugin.uvc.profile; - import android.content.Intent; +import android.graphics.Rect; import android.os.Bundle; -import android.util.Log; +import android.util.Size; -import org.deviceconnect.android.deviceplugin.uvc.UVCDeviceService; +import org.deviceconnect.android.deviceplugin.uvc.profile.utils.H264Level; +import org.deviceconnect.android.deviceplugin.uvc.profile.utils.H264Profile; +import org.deviceconnect.android.deviceplugin.uvc.profile.utils.H265Level; +import org.deviceconnect.android.deviceplugin.uvc.profile.utils.H265Profile; +import org.deviceconnect.android.deviceplugin.uvc.recorder.Broadcaster; import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; -import org.deviceconnect.android.deviceplugin.uvc.recorder.UVCRecorder; -import org.deviceconnect.android.deviceplugin.uvc.recorder.preview.PreviewServer; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorderManager; +import org.deviceconnect.android.deviceplugin.uvc.recorder.PreviewServer; +import org.deviceconnect.android.deviceplugin.uvc.recorder.uvc.UvcRecorder; import org.deviceconnect.android.deviceplugin.uvc.service.UVCService; +import org.deviceconnect.android.deviceplugin.uvc.util.CapabilityUtil; import org.deviceconnect.android.message.MessageUtils; import org.deviceconnect.android.profile.MediaStreamRecordingProfile; -import org.deviceconnect.android.profile.api.DConnectApi; import org.deviceconnect.android.profile.api.DeleteApi; import org.deviceconnect.android.profile.api.GetApi; import org.deviceconnect.android.profile.api.PutApi; @@ -26,10 +31,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import javax.net.ssl.SSLContext; /** * UVC MediaStream Recording Profile. @@ -37,287 +38,708 @@ * @author NTT DOCOMO, INC. */ public class UVCMediaStreamRecordingProfile extends MediaStreamRecordingProfile { - private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); - private final DConnectApi mGetMediaRecorderApi = new GetApi() { + public UVCMediaStreamRecordingProfile() { + // GET /gotapi/mediaStreamRecording/mediaRecorder + addApi(new GetApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_MEDIARECORDER; + } - @Override - public String getAttribute() { - return ATTRIBUTE_MEDIARECORDER; - } + @Override + public boolean onRequest(final Intent request, final Intent response) { + UVCService service = (UVCService) getService(); - @Override - public boolean onRequest(final Intent request, final Intent response) { - mExecutor.execute(() -> { - try { - UVCRecorder recorder = getUVCRecorder(); - if (recorder == null) { - MessageUtils.setNotFoundServiceError(response); - return; - } + if (service == null) { + MessageUtils.setNotFoundServiceError(response, "service is not found."); + return true; + } - if (!getService().isOnline()) { - MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); - return; - } + if (!getService().isOnline()) { + MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); + return true; + } + + setResult(response, DConnectMessage.RESULT_OK); + setMediaRecorders(response, service.getUvcRecorderList()); + return true; + } + }); + + // GET /gotapi/mediaStreamRecording/options + addApi(new GetApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_OPTIONS; + } - setMediaRecorders(response, recorder); - setResult(response, DConnectMessage.RESULT_OK); - } finally { - sendResponse(response); + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + + UVCService service = (UVCService) getService(); + if (service == null) { + MessageUtils.setNotFoundServiceError(response, "service is not found."); + return true; } - }); - return false; - } - }; - private final DConnectApi mGetOptionsApi = new GetApi() { + if (!service.isOnline()) { + MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); + return true; + } - @Override - public String getAttribute() { - return ATTRIBUTE_OPTIONS; - } + UvcRecorder recorder = getUvcRecorderByTarget(target); + if (recorder == null) { + MessageUtils.setNotFoundServiceError(response, target + " is not found."); + return true; + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - mExecutor.execute(() -> { - try { - if (!getService().isOnline()) { - MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); - return; + setResult(response, DConnectMessage.RESULT_OK); + setOptions(response, recorder); + return true; + } + }); + + // PUT /gotapi/mediaStreamRecording/options + addApi(new PutApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_OPTIONS; + } + + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + Integer imageWidth = getImageWidth(request); + Integer imageHeight = getImageHeight(request); + Integer previewWidth = getPreviewWidth(request); + Integer previewHeight = getPreviewHeight(request); + Double previewMaxFrameRate = getPreviewMaxFrameRate(request); + Integer previewBitRate = parseInteger(request, "previewBitRate"); + Integer previewKeyFrameInterval = parseInteger(request, "previewKeyFrameInterval"); + String previewEncoder = request.getStringExtra("previewEncoder"); + String previewProfile = request.getStringExtra("previewProfile"); + String previewLevel = request.getStringExtra("previewLevel"); + Integer previewIntraRefresh = parseInteger("previewIntraRefresh"); + Double previewJpegQuality = parseDouble(request, "previewJpegQuality"); + Integer previewClipLeft = parseInteger(request, "previewClipLeft"); + Integer previewClipTop = parseInteger(request, "previewClipTop"); + Integer previewClipRight = parseInteger(request, "previewClipRight"); + Integer previewClipBottom = parseInteger(request, "previewClipBottom"); + Boolean previewClipReset = parseBoolean(request, "previewClipReset"); + MediaRecorder.ProfileLevel profileLevel = null; + Rect drawingRect = null; + + UVCService service = (UVCService) getService(); + if (service == null) { + MessageUtils.setNotFoundServiceError(response, "service is not found."); + return true; + } + + if (!service.isOnline()) { + MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); + return true; + } + + UvcRecorder recorder = getUvcRecorderByTarget(target); + if (recorder == null) { + MessageUtils.setNotFoundServiceError(response, target + " is not found."); + return true; + } + + MediaRecorder.Settings settings = recorder.getSettings(); + + // 値の妥当性チェック + + if (imageWidth == null && imageHeight != null || + imageWidth != null && imageHeight == null) { + MessageUtils.setInvalidRequestParameterError(response, + "imageWidth or imageHeight is not set."); + return true; + } + + if (previewWidth == null && previewHeight != null || + previewWidth != null && previewHeight == null) { + MessageUtils.setInvalidRequestParameterError(response, + "previewWidth or previewHeight is not set."); + return true; + } + + if (imageWidth != null && !settings.isSupportedPictureSize(imageWidth, imageHeight)) { + MessageUtils.setInvalidRequestParameterError(response, + "imageWidth or imageHeight is not support value."); + return true; + } + + if (previewWidth != null && !settings.isSupportedPreviewSize(previewWidth, previewHeight)) { + MessageUtils.setInvalidRequestParameterError(response, + "previewWidth or previewHeight is not support value."); + return true; + } + + if (previewEncoder != null && !settings.isSupportedVideoEncoder(previewEncoder)) { + MessageUtils.setInvalidRequestParameterError(response, + "Unsupported preview encoder: " + previewEncoder); + return true; + } + + if (previewProfile != null || previewLevel != null) { + if (previewProfile == null) { + MessageUtils.setInvalidRequestParameterError(response, + "previewProfile is not set."); + return true; } - UVCRecorder recorder = getUVCRecorder(); - if (recorder == null) { - MessageUtils.setNotFoundServiceError(response); - return; + if (previewLevel == null) { + MessageUtils.setInvalidRequestParameterError(response, + "previewLevel is not set."); + return true; } - setOptions(response, recorder); - setResult(response, DConnectMessage.RESULT_OK); - } finally { - sendResponse(response); + MediaRecorder.VideoEncoderName encoderName = settings.getPreviewEncoderName(); + if (previewEncoder != null) { + encoderName = MediaRecorder.VideoEncoderName.nameOf(previewEncoder); + } + switch (encoderName) { + case H264: { + H264Profile p = H264Profile.nameOf(previewProfile); + H264Level l = H264Level.nameOf(previewLevel); + if (p == null || l == null || !settings.isSupportedProfileLevel(p.getValue(), l.getValue())) { + MessageUtils.setInvalidRequestParameterError(response, + "Unsupported preview profile and level: " + previewProfile + " - " + previewLevel); + return true; + } + profileLevel = new MediaRecorder.ProfileLevel(p.getValue(), l.getValue()); + } break; + case H265: { + H265Profile p = H265Profile.nameOf(previewProfile); + H265Level l = H265Level.nameOf(previewLevel); + if (p == null || l == null || !settings.isSupportedProfileLevel(p.getValue(), l.getValue())) { + MessageUtils.setInvalidRequestParameterError(response, + "Unsupported preview profile and level: " + previewProfile + " - " + previewLevel); + return true; + } + profileLevel = new MediaRecorder.ProfileLevel(p.getValue(), l.getValue()); + } break; + } } - }); - return false; - } - }; - private final DConnectApi mPutOptionsApi = new PutApi() { + if (previewJpegQuality != null && (previewJpegQuality < 0.0 || previewJpegQuality > 1.0)) { + MessageUtils.setInvalidRequestParameterError(response, + "previewJpegQuality is invalid. value=" + previewJpegQuality); + return true; + } - @Override - public String getAttribute() { - return ATTRIBUTE_OPTIONS; - } + if (previewClipLeft != null || previewClipTop != null + || previewClipRight != null || previewClipBottom != null) { - @Override - public boolean onRequest(final Intent request, final Intent response) { - mExecutor.execute(() -> { - try { - Integer imageWidth = getImageWidth(request); - Integer imageHeight = getImageHeight(request); - Integer previewWidth = getPreviewWidth(request); - Integer previewHeight = getPreviewHeight(request); - Double previewMaxFrameRate = getPreviewMaxFrameRate(request); - - UVCRecorder recorder = getUVCRecorder(); - if (recorder == null) { - MessageUtils.setNotFoundServiceError(response); - return; + if (previewClipLeft == null) { + MessageUtils.setInvalidRequestParameterError(response, + "previewClipLeft is not set."); + return true; } - if (!getService().isOnline()) { - MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); - return; + if (previewClipTop == null) { + MessageUtils.setInvalidRequestParameterError(response, + "previewClipTop is not set."); + return true; } - if (imageWidth == null && imageHeight != null || - imageWidth != null && imageHeight == null) { + if (previewClipRight == null) { MessageUtils.setInvalidRequestParameterError(response, - "imageWidth or imageHeight is not set."); - return; + "previewClipRight is not set."); + return true; } - if (previewWidth == null && previewHeight != null || - previewWidth != null && previewHeight == null) { + if (previewClipBottom == null) { MessageUtils.setInvalidRequestParameterError(response, - "previewWidth or previewHeight is not set."); - return; + "previewClipBottom is not set."); + return true; } - if (imageWidth != null && imageHeight != null) { - recorder.setPictureSize(new MediaRecorder.Size(imageWidth, imageHeight)); + if (previewClipLeft < 0) { + MessageUtils.setInvalidRequestParameterError(response, + "previewClipLeft cannot set a negative value."); + return true; } - if (previewWidth != null && previewHeight != null) { - recorder.setPreviewSize(new MediaRecorder.Size(previewWidth, previewHeight)); + if (previewClipBottom < 0) { + MessageUtils.setInvalidRequestParameterError(response, + "previewClipBottom cannot set a negative value."); + return true; } - if (previewMaxFrameRate != null) { - recorder.setMaxFrameRate(previewMaxFrameRate); + if (previewClipLeft >= previewClipRight) { + MessageUtils.setInvalidRequestParameterError(response, + "previewClipLeft is larger than previewClipRight."); + return true; } - - setResult(response, DConnectMessage.RESULT_OK); - } finally { - sendResponse(response); + + if (previewClipTop >= previewClipBottom) { + MessageUtils.setInvalidRequestParameterError(response, + "previewClipTop is larger than previewClipBottom."); + return true; + } + + drawingRect = new Rect(previewClipLeft, previewClipTop, previewClipRight, previewClipBottom); } - }); - return false; - } - }; - private final DConnectApi mPutPreviewApi = new PutApi() { + // 設定 - @Override - public String getAttribute() { - return ATTRIBUTE_PREVIEW; - } + if (imageWidth != null) { + settings.setPictureSize(new Size(imageWidth, imageHeight)); + } + + if (previewWidth != null) { + settings.setPreviewSize(new Size(previewWidth, previewHeight)); + } + + if (previewMaxFrameRate != null) { + settings.setPreviewMaxFrameRate(previewMaxFrameRate.intValue()); + } + + if (previewBitRate != null) { + settings.setPreviewBitRate(previewBitRate * 1024); + } + + if (previewKeyFrameInterval != null) { + settings.setPreviewKeyFrameInterval(previewKeyFrameInterval); + } + + if (previewEncoder != null) { + settings.setPreviewEncoder(previewEncoder); + // エンコーダが切り替えられた場合は、プロファイル・レベルは設定無しにする + settings.setProfileLevel(null); + } + + if (profileLevel != null) { + settings.setProfileLevel(profileLevel); + } + + if (previewIntraRefresh != null) { + settings.setIntraRefresh(previewIntraRefresh); + } + + if (previewJpegQuality != null) { + settings.setPreviewQuality((int) (previewJpegQuality * 100)); + } + + if (previewClipReset != null && previewClipReset) { + settings.setDrawingRange(null); + } else if (drawingRect != null) { + settings.setDrawingRange(drawingRect); + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - mExecutor.execute(() -> { try { - if (!getService().isOnline()) { - MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); - return; + recorder.onConfigChange(); + } catch (Exception e) { + MessageUtils.setIllegalDeviceStateError(response, "Failed to change a config."); + return true; + } + + setResult(response, DConnectMessage.RESULT_OK); + return true; + } + }); + + // PUT /gotapi/mediaStreamRecording/preview + addApi(new PutApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_PREVIEW; + } + + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + + UVCService service = (UVCService) getService(); + if (service == null) { + MessageUtils.setNotFoundServiceError(response, "service is not found."); + return true; + } + + if (!service.isOnline()) { + MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); + return true; + } + + UvcRecorder recorder = getUvcRecorderByTarget(target); + if (recorder == null) { + MessageUtils.setNotFoundServiceError(response, target + " is not found."); + return true; + } + + MediaRecorderManager mrm = service.getMediaRecorderManager(); + if (!mrm.canUseRecorder(recorder)) { + // 他のカメラが使用中の場合はエラーを返却 + MessageUtils.setIllegalDeviceStateError(response, "Other recorder are being used."); + return true; + } + + recorder.requestPermission(new MediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + List servers = recorder.startPreview(); + if (servers.isEmpty()) { + MessageUtils.setIllegalDeviceStateError(response, + "Failed to start a preview server."); + } else { + setResult(response, DConnectMessage.RESULT_OK); + setUri(response, getDefaultUri(servers)); + setStreams(response, servers); + } + sendResponse(response); } - UVCRecorder recorder = getUVCRecorder(); - List servers = recorder.startPreview(); - - if (servers.isEmpty()) { - MessageUtils.setIllegalDeviceStateError(response, "Failed to start a preview server."); - } else { - UVCDeviceService plugin = (UVCDeviceService) getContext(); - plugin.getSSLContext(new UVCDeviceService.SSLContextCallback() { - @Override - public void onGet(final SSLContext sslContext) { - startPreviewServers(sslContext, response); - } - @Override - public void onError() { - startPreviewServers(null, response); - } - }); + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, + "Permission for camera is not granted."); + sendResponse(response); } - } finally { - sendResponse(response); + }); + + return false; + } + }); + + // DELETE /gotapi/mediaStreamRecording/preview + addApi(new DeleteApi() { + + @Override + public String getAttribute() { + return ATTRIBUTE_PREVIEW; + } + + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + + UVCService service = (UVCService) getService(); + if (service == null) { + MessageUtils.setNotFoundServiceError(response, "service is not found."); + return true; } - }); - return false; - } - }; - private final DConnectApi mDeletePreviewApi = new DeleteApi() { + if (!service.isOnline()) { + MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); + return true; + } - @Override - public String getAttribute() { - return ATTRIBUTE_PREVIEW; - } + UvcRecorder recorder = getUvcRecorderByTarget(target); + if (recorder == null) { + MessageUtils.setNotFoundServiceError(response, target + " is not found."); + return true; + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - mExecutor.execute(() -> { - try { - UVCRecorder recorder = getUVCRecorder(); - if (recorder != null) { + recorder.requestPermission(new MediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { recorder.stopPreview(); + setResult(response, DConnectMessage.RESULT_OK); + sendResponse(response); } - if (!getService().isOnline()) { - MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); - return; + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, + "Permission for camera is not granted."); + sendResponse(response); } + }); + return false; + } + }); + + // PUT /gotapi/mediaStreamRecording/broadcast + addApi(new PutApi() { + @Override + public String getAttribute() { + return "broadcast"; + } - setResult(response, DConnectMessage.RESULT_OK); - } finally { - sendResponse(response); + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + String broadcastURI = request.getStringExtra("uri"); + + if (broadcastURI == null) { + MessageUtils.setInvalidRequestParameterError(response, "broadcastURI "); + return true; } - }); - return false; - } - }; - public UVCMediaStreamRecordingProfile() { - addApi(mGetMediaRecorderApi); - addApi(mGetOptionsApi); - addApi(mPutOptionsApi); - addApi(mPutPreviewApi); - addApi(mDeletePreviewApi); + UVCService service = (UVCService) getService(); + if (service == null) { + MessageUtils.setNotFoundServiceError(response, "service is not found."); + return true; + } + + if (!service.isOnline()) { + MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); + return true; + } + + UvcRecorder recorder = getUvcRecorderByTarget(target); + if (recorder == null) { + MessageUtils.setNotFoundServiceError(response, target + " is not found."); + return true; + } + + if (recorder.isBroadcasterRunning()) { + MessageUtils.setIllegalDeviceStateError(response, "broadcast is already running."); + return true; + } + + MediaRecorderManager mrm = service.getMediaRecorderManager(); + if (!mrm.canUseRecorder(recorder)) { + // 他のカメラが使用中の場合はエラーを返却 + MessageUtils.setIllegalDeviceStateError(response, "Other recorder are being used."); + return true; + } + + recorder.requestPermission(new MediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + // TODO 排他的に処理を行うようにします。 + if (recorder instanceof UvcRecorder) { + // 使用していない場合は停止する + mrm.stopCameraRecorder(recorder); + } + + Broadcaster b = recorder.startBroadcaster(broadcastURI); + if (b != null) { + setResult(response, DConnectMessage.RESULT_OK); + } else { + MessageUtils.setIllegalServerStateError(response, + "Failed to start a broadcast."); + } + sendResponse(response); + } + + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, + "Permission for camera is not granted."); + sendResponse(response); + } + }); + return false; + } + }); + + // DELETE /gotapi/mediaStreamRecording/broadcast + addApi(new DeleteApi() { + @Override + public String getAttribute() { + return "broadcast"; + } + + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + + UVCService service = (UVCService) getService(); + if (service == null) { + MessageUtils.setNotFoundServiceError(response, "service is not found."); + return true; + } + + if (!service.isOnline()) { + MessageUtils.setIllegalDeviceStateError(response, "device is not connected."); + return true; + } + + UvcRecorder recorder = getUvcRecorderByTarget(target); + if (recorder == null) { + MessageUtils.setNotFoundServiceError(response, target + " is not found."); + return true; + } + + recorder.requestPermission(new MediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + recorder.stopBroadcaster(); + + setResult(response, DConnectMessage.RESULT_OK); + sendResponse(response); + } + + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, + "Permission for camera is not granted."); + sendResponse(response); + } + }); + return false; + } + }); } - private UVCRecorder getUVCRecorder() { + private UvcRecorder getUvcRecorder() { UVCService service = (UVCService) getService(); - return service != null ? service.getUVCRecorder() : null; + if (service != null) { + return service.getDefaultRecorder(); + } + return null; } - private static void setMediaRecorders(final Intent response, final UVCRecorder uvcRecorder) { + private UvcRecorder getUvcRecorderByTarget(String target) { + if (target == null) { + return getUvcRecorder(); + } + + UVCService service = (UVCService) getService(); + if (service != null) { + return service.findUvcRecorderById(target); + } + return null; + } + + private static void setMediaRecorders(final Intent response, final List uvcRecorders) { List recorderList = new ArrayList<>(); - Bundle recorder = new Bundle(); - setMediaRecorder(recorder, uvcRecorder); - recorderList.add(recorder); + for (UvcRecorder uvcRecorder : uvcRecorders) { + Bundle recorder = new Bundle(); + setMediaRecorder(recorder, uvcRecorder); + recorderList.add(recorder); + } setRecorders(response, recorderList); } - private static void setMediaRecorder(final Bundle recorder, final UVCRecorder uvcRecorder) { - MediaRecorder.Size previewSize = uvcRecorder.getPreviewSize(); - - setRecorderId(recorder, uvcRecorder.getId()); - setRecorderName(recorder, uvcRecorder.getName()); - setRecorderState(recorder, uvcRecorder.isStartedPreview() ? RecorderState.RECORDING - : RecorderState.INACTIVE); - setRecorderPreviewWidth(recorder, previewSize.getWidth()); - setRecorderPreviewHeight(recorder, previewSize.getHeight()); - setRecorderPreviewMaxFrameRate(recorder, uvcRecorder.getMaxFrameRate()); - setRecorderMIMEType(recorder, uvcRecorder.getMimeType()); - setRecorderConfig(recorder, ""); + private static void setMediaRecorder(final Bundle info, final UvcRecorder recorder) { + MediaRecorder.Settings settings = recorder.getSettings(); + Size previewSize = settings.getPreviewSize(); + + setRecorderId(info, recorder.getId()); + setRecorderName(info, recorder.getName()); + setRecorderState(info, recorder.isPreviewRunning() ? RecorderState.RECORDING : RecorderState.INACTIVE); + setRecorderPreviewWidth(info, previewSize.getWidth()); + setRecorderPreviewHeight(info, previewSize.getHeight()); + setRecorderPreviewMaxFrameRate(info, settings.getPreviewMaxFrameRate()); + setRecorderMIMEType(info, recorder.getMimeType()); + setRecorderConfig(info, ""); + info.putInt("previewBitRate", settings.getPreviewBitRate() / 1024); + info.putInt("previewKeyFrameInterval", settings.getPreviewKeyFrameInterval()); + info.putString("previewEncoder", settings.getPreviewEncoder()); + info.putString("previewEncoder", settings.getPreviewEncoder()); + info.putFloat("previewJpegQuality", settings.getPreviewQuality() / 100.0f); + MediaRecorder.ProfileLevel pl = settings.getProfileLevel(); + if (pl != null) { + switch (MediaRecorder.VideoEncoderName.nameOf(settings.getPreviewEncoder())) { + case H264: + info.putString("previewProfile", H264Profile.valueOf(pl.getProfile()).getName()); + info.putString("previewLevel", H264Level.valueOf(pl.getLevel()).getName()); + break; + case H265: + info.putString("previewProfile", H265Profile.valueOf(pl.getProfile()).getName()); + info.putString("previewLevel", H265Level.valueOf(pl.getLevel()).getName()); + break; + } + } + Bundle status = new Bundle(); + status.putBoolean("preview", recorder.isPreviewRunning()); + status.putBoolean("broadcast", recorder.isBroadcasterRunning()); +// status.putBoolean("recording", recorder.getState() == MediaRecorder.State.RECORDING); + info.putParcelable("status", status); + + // 切り抜き設定 + Rect rect = settings.getDrawingRange(); + if (rect != null) { + Bundle drawingRect = new Bundle(); + drawingRect.putInt("left", rect.left); + drawingRect.putInt("top", rect.top); + drawingRect.putInt("right", rect.right); + drawingRect.putInt("bottom", rect.bottom); + info.putBundle("previewClip", drawingRect); + } } - private static void setOptions(final Intent response, final UVCRecorder recorder) { - List options = recorder.getSupportedPreviewSizes(); + private static void setOptions(final Intent response, final UvcRecorder recorder) { + List options = recorder.getSettings().getSupportedPreviewSizes(); List previewSizes = new ArrayList<>(); - for (MediaRecorder.Size option : options) { + for (Size option : options) { Bundle size = new Bundle(); setWidth(size, option.getWidth()); setHeight(size, option.getHeight()); previewSizes.add(size); } setPreviewSizes(response, previewSizes); - setMIMEType(response, recorder.getSupportedMimeTypes()); + setMIMEType(response, recorder.getServerProvider().getSupportedMimeType()); + List encoders = new ArrayList<>(); + for (String name : recorder.getSettings().getSupportedVideoEncoders()) { + MediaRecorder.VideoEncoderName encoderName = MediaRecorder.VideoEncoderName.nameOf(name); + Bundle encoder = new Bundle(); + encoder.putString("name", name); + encoder.putParcelableArray("profileLevel", getProfileLevels(encoderName)); + encoders.add(encoder); + } + response.putExtra("encoder", encoders.toArray(new Bundle[0])); } private static void setMIMEType(final Intent response, final List mimeTypes) { response.putExtra("mimeType", mimeTypes.toArray(new String[0])); } - private void startPreviewServers(final SSLContext sslContext, - final Intent response) { - // SSLContext を設定 - for (PreviewServer server : getUVCRecorder().getServers()) { - if (sslContext != null && server.usesSSLContext()) { - server.setSSLContext(sslContext); + private static String getDefaultUri(List servers) { + String defaultUri = null; + for (PreviewServer server : servers) { + // Motion-JPEG をデフォルトの値として使用します + if (defaultUri == null && "video/x-mjpeg".equals(server.getMimeType())) { + defaultUri = server.getUri(); } } - String defaultUri = null; + return defaultUri != null ? defaultUri : ""; + } + + private static void setStreams(Intent response, List servers) { + response.putExtra("streams", createStreams(servers)); + } + + private static Bundle[] createStreams(List servers) { List streams = new ArrayList<>(); - List servers = getUVCRecorder().startPreview(); - if (servers.isEmpty()) { - MessageUtils.setIllegalServerStateError(response, "Failed to start web server."); - } else { - for (PreviewServer server : servers) { - // Motion-JPEG をデフォルトの値として使用します - if (defaultUri == null && "video/x-mjpeg".equals(server.getMimeType())) { - defaultUri = server.getUrl(); - } - - Bundle stream = new Bundle(); - stream.putString("mimeType", server.getMimeType()); - stream.putString("uri", server.getUrl()); - streams.add(stream); + for (PreviewServer server : servers) { + Bundle stream = new Bundle(); + stream.putString("mimeType", server.getMimeType()); + stream.putString("uri", server.getUri()); + streams.add(stream); + } + return streams.toArray(new Bundle[0]); + } + + /** + * エンコーダがサポートしているプロファイルとレベルを格納した Bundle の配列を取得します. + * + * @param encoderName エンコーダ + * @return プロファイルとレベルを格納した Bundle の配列 + */ + private static Bundle[] getProfileLevels(MediaRecorder.VideoEncoderName encoderName) { + List list = new ArrayList<>(); + for (MediaRecorder.ProfileLevel pl : CapabilityUtil.getSupportedProfileLevel(encoderName.getMimeType())) { + switch (encoderName) { + case H264: { + H264Profile p = H264Profile.valueOf(pl.getProfile()); + H264Level l = H264Level.valueOf(pl.getLevel()); + if (p != null && l != null) { + Bundle encoder = new Bundle(); + encoder.putString("profile", p.getName()); + encoder.putString("level", l.getName()); + list.add(encoder); + } + } break; + case H265: { + H265Profile p = H265Profile.valueOf(pl.getProfile()); + H265Level l = H265Level.valueOf(pl.getLevel()); + if (p != null && l != null) { + Bundle encoder = new Bundle(); + encoder.putString("profile", p.getName()); + encoder.putString("level", l.getName()); + list.add(encoder); + } + } break; } - setResult(response, DConnectMessage.RESULT_OK); - setUri(response, defaultUri != null ? defaultUri : ""); - response.putExtra("streams", streams.toArray(new Bundle[streams.size()])); } + return list.toArray(new Bundle[0]); } } diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/UVCSystemProfile.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/UVCSystemProfile.java index 9ccdc22bf9..6b8d67aac5 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/UVCSystemProfile.java +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/UVCSystemProfile.java @@ -10,7 +10,7 @@ import android.content.Intent; import android.os.Bundle; -import org.deviceconnect.android.deviceplugin.uvc.activity.UVCServiceListActivity; +import org.deviceconnect.android.deviceplugin.uvc.activity.UVCSettingsActivity; import org.deviceconnect.android.profile.SystemProfile; /** @@ -19,11 +19,8 @@ * @author NTT DOCOMO, INC. */ public class UVCSystemProfile extends SystemProfile { - @Override - protected Class getSettingPageActivity(final Intent request, - final Bundle param) { - return UVCServiceListActivity.class; + protected Class getSettingPageActivity(Intent request, Bundle param) { + return UVCSettingsActivity.class; } - } diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H264Level.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H264Level.java new file mode 100644 index 0000000000..5925ee40ca --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H264Level.java @@ -0,0 +1,60 @@ +package org.deviceconnect.android.deviceplugin.uvc.profile.utils; + +import android.media.MediaCodecInfo; + +public enum H264Level { + AVCLevel1("AVCLevel1", MediaCodecInfo.CodecProfileLevel.AVCLevel1), + AVCLevel11("AVCLevel1", MediaCodecInfo.CodecProfileLevel.AVCLevel1), + AVCLevel12("AVCLevel12", MediaCodecInfo.CodecProfileLevel.AVCLevel12), + AVCLevel13("AVCLevel13", MediaCodecInfo.CodecProfileLevel.AVCLevel13), + AVCLevel1b("AVCLevel1b", MediaCodecInfo.CodecProfileLevel.AVCLevel1b), + AVCLevel2("AVCLevel2", MediaCodecInfo.CodecProfileLevel.AVCLevel2), + AVCLevel21("AVCLevel21", MediaCodecInfo.CodecProfileLevel.AVCLevel21), + AVCLevel22("AVCLevel22", MediaCodecInfo.CodecProfileLevel.AVCLevel22), + AVCLevel3("AVCLevel3", MediaCodecInfo.CodecProfileLevel.AVCLevel3), + AVCLevel31("AVCLevel31", MediaCodecInfo.CodecProfileLevel.AVCLevel31), + AVCLevel32("AVCLevel32", MediaCodecInfo.CodecProfileLevel.AVCLevel32), + AVCLevel4("AVCLevel4", MediaCodecInfo.CodecProfileLevel.AVCLevel4), + AVCLevel41("AVCLevel41", MediaCodecInfo.CodecProfileLevel.AVCLevel41), + AVCLevel42("AVCLevel42", MediaCodecInfo.CodecProfileLevel.AVCLevel42), + AVCLevel5("AVCLevel5", MediaCodecInfo.CodecProfileLevel.AVCLevel5), + AVCLevel51("AVCLevel51", MediaCodecInfo.CodecProfileLevel.AVCLevel51), + AVCLevel52("AVCLevel52", MediaCodecInfo.CodecProfileLevel.AVCLevel52), + AVCLevel6("AVCLevel6", MediaCodecInfo.CodecProfileLevel.AVCLevel6), + AVCLevel61("AVCLevel61", MediaCodecInfo.CodecProfileLevel.AVCLevel61), + AVCLevel62("AVCLevel62", MediaCodecInfo.CodecProfileLevel.AVCLevel62); + + private final String mName; + private final int mValue; + + H264Level(String name, int value) { + mName = name; + mValue = value; + } + + public String getName() { + return mName; + } + + public int getValue() { + return mValue; + } + + public static H264Level nameOf(String name) { + for (H264Level l : values()) { + if (l.mName.equalsIgnoreCase(name)) { + return l; + } + } + return null; + } + + public static H264Level valueOf(int value) { + for (H264Level l : values()) { + if (l.mValue == value) { + return l; + } + } + return null; + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H264Profile.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H264Profile.java new file mode 100644 index 0000000000..6f8d8cd54f --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H264Profile.java @@ -0,0 +1,49 @@ +package org.deviceconnect.android.deviceplugin.uvc.profile.utils; + +import android.media.MediaCodecInfo; + +public enum H264Profile { + AVCProfileBaseline("AVCProfileBaseline", MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline), + AVCProfileConstrainedBaseline("AVCProfileConstrainedBaseline", MediaCodecInfo.CodecProfileLevel.AVCProfileConstrainedBaseline), + AVCProfileConstrainedHigh("AVCProfileConstrainedHigh", MediaCodecInfo.CodecProfileLevel.AVCProfileConstrainedHigh), + AVCProfileExtended("AVCProfileExtended", MediaCodecInfo.CodecProfileLevel.AVCProfileExtended), + AVCProfileHigh("AVCProfileHigh", MediaCodecInfo.CodecProfileLevel.AVCProfileHigh), + AVCProfileHigh10("AVCProfileHigh10", MediaCodecInfo.CodecProfileLevel.AVCProfileHigh10), + AVCProfileHigh422("AVCProfileHigh422", MediaCodecInfo.CodecProfileLevel.AVCProfileHigh422), + AVCProfileHigh444("AVCProfileHigh444", MediaCodecInfo.CodecProfileLevel.AVCProfileHigh444), + AVCProfileMain("AVCProfileMain", MediaCodecInfo.CodecProfileLevel.AVCProfileMain); + + private final String mName; + private final int mValue; + + H264Profile(String name, int value) { + mName = name; + mValue = value; + } + + public String getName() { + return mName; + } + + public int getValue() { + return mValue; + } + + public static H264Profile nameOf(String name) { + for (H264Profile p : values()) { + if (p.mName.equalsIgnoreCase(name)) { + return p; + } + } + return null; + } + + public static H264Profile valueOf(int value) { + for (H264Profile l : values()) { + if (l.mValue == value) { + return l; + } + } + return null; + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H265Level.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H265Level.java new file mode 100644 index 0000000000..2421c31571 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H265Level.java @@ -0,0 +1,66 @@ +package org.deviceconnect.android.deviceplugin.uvc.profile.utils; + +import android.media.MediaCodecInfo; + +public enum H265Level { + HEVCHighTierLevel1("HEVCHighTierLevel1", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel1), + HEVCHighTierLevel2("HEVCHighTierLevel2", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel2), + HEVCHighTierLevel21("HEVCHighTierLevel21", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel21), + HEVCHighTierLevel3("HEVCHighTierLevel3", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel3), + HEVCHighTierLevel31("HEVCHighTierLevel31", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel31), + HEVCHighTierLevel4("HEVCHighTierLevel4", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel4), + HEVCHighTierLevel41("HEVCHighTierLevel41", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel41), + HEVCHighTierLevel5("HEVCHighTierLevel5", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel5), + HEVCHighTierLevel51("HEVCHighTierLevel51", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel51), + HEVCHighTierLevel52("HEVCHighTierLevel52", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel52), + HEVCHighTierLevel6("HEVCHighTierLevel6", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel6), + HEVCHighTierLevel61("HEVCHighTierLevel61", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel61), + HEVCHighTierLevel62("HEVCHighTierLevel62", MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel62), + HEVCMainTierLevel1("HEVCMainTierLevel1", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel1), + HEVCMainTierLevel2("HEVCMainTierLevel2", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel2), + HEVCMainTierLevel21("HEVCMainTierLevel21", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel21), + HEVCMainTierLevel3("HEVCMainTierLevel3", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel3), + HEVCMainTierLevel31("HEVCMainTierLevel31", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel31), + HEVCMainTierLevel4("HEVCMainTierLevel4", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel4), + HEVCMainTierLevel41("HEVCMainTierLevel41", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel41), + HEVCMainTierLevel5("HEVCMainTierLevel5", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel5), + HEVCMainTierLevel51("HEVCMainTierLevel51", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel51), + HEVCMainTierLevel52("HEVCMainTierLevel52", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel52), + HEVCMainTierLevel6("HEVCMainTierLevel6", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel6), + HEVCMainTierLevel61("HEVCMainTierLevel61", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel61), + HEVCMainTierLevel62("HEVCMainTierLevel62", MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel62); + + private final String mName; + private final int mValue; + + H265Level(String name, int value) { + mName = name; + mValue = value; + } + + public String getName() { + return mName; + } + + public int getValue() { + return mValue; + } + + public static H265Level nameOf(String name) { + for (H265Level l : values()) { + if (l.mName.equalsIgnoreCase(name)) { + return l; + } + } + return null; + } + + public static H265Level valueOf(int value) { + for (H265Level l : values()) { + if (l.mValue == value) { + return l; + } + } + return null; + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H265Profile.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H265Profile.java new file mode 100644 index 0000000000..2ffdaaebbd --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/profile/utils/H265Profile.java @@ -0,0 +1,45 @@ +package org.deviceconnect.android.deviceplugin.uvc.profile.utils; + +import android.media.MediaCodecInfo; + +public enum H265Profile { + HEVCProfileMain("HEVCProfileMain", MediaCodecInfo.CodecProfileLevel.HEVCProfileMain), + HEVCProfileMain10("HEVCProfileMain10", MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10), + HEVCProfileMain10HDR10("HEVCProfileMain10HDR10", MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10), + HEVCProfileMain10HDR10Plus("HEVCProfileMain10HDR10Plus", MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10Plus), + HEVCProfileMainStill("HEVCProfileMainStill", MediaCodecInfo.CodecProfileLevel.HEVCProfileMainStill); + + private final String mName; + private final int mValue; + + H265Profile(String name, int value) { + mName = name; + mValue = value; + } + + public String getName() { + return mName; + } + + public int getValue() { + return mValue; + } + + public static H265Profile nameOf(String name) { + for (H265Profile p : values()) { + if (p.mName.equalsIgnoreCase(name)) { + return p; + } + } + return null; + } + + public static H265Profile valueOf(int value) { + for (H265Profile l : values()) { + if (l.mValue == value) { + return l; + } + } + return null; + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractBroadcastProvider.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractBroadcastProvider.java new file mode 100644 index 0000000000..63a7abba67 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractBroadcastProvider.java @@ -0,0 +1,269 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +import androidx.core.app.NotificationCompat; + +import org.deviceconnect.android.deviceplugin.uvc.R; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class AbstractBroadcastProvider implements BroadcasterProvider { + /** + * 映像を配信するクラス. + */ + private Broadcaster mBroadcaster; + + /** + * イベントを通知するリスナー. + */ + private OnEventListener mOnEventListener; + + private final Context mContext; + private final MediaRecorder mRecorder; + + public AbstractBroadcastProvider(Context context, MediaRecorder recorder) { + mContext = context; + mRecorder = recorder; + } + + @Override + public void setOnEventListener(OnEventListener listener) { + mOnEventListener = listener; + } + + @Override + public Broadcaster getBroadcaster() { + return mBroadcaster; + } + + @Override + public boolean isRunning() { + return mBroadcaster != null && mBroadcaster.isRunning(); + } + + @Override + public Broadcaster startBroadcaster(String broadcastURI) { + if (broadcastURI == null) { + return null; + } + + if (mBroadcaster != null && mBroadcaster.isRunning()) { + return mBroadcaster; + } + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean result = new AtomicBoolean(false); + + mBroadcaster = createBroadcaster(broadcastURI); + if (mBroadcaster == null) { + return null; + } + mBroadcaster.setOnEventListener(new Broadcaster.OnEventListener() { + @Override + public void onStarted() { + postBroadcastStarted(mBroadcaster); + } + + @Override + public void onStopped() { + hideNotification(mRecorder.getId()); + postBroadcastStopped(mBroadcaster); + } + + @Override + public void onError(Exception e) { + postBroadcastError(mBroadcaster, e); + } + }); + + mBroadcaster.start(new Broadcaster.OnStartCallback() { + @Override + public void onSuccess() { + result.set(true); + latch.countDown(); + } + + @Override + public void onFailed(Exception e) { + result.set(false); + latch.countDown(); + } + }); + + try { + latch.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return null; + } + + if (!result.get()) { + mBroadcaster.stop(); + mBroadcaster = null; + } else { + sendNotification(mRecorder.getId(), mRecorder.getName()); + } + + return mBroadcaster; + } + + @Override + public void stopBroadcaster() { + hideNotification(mRecorder.getId()); + + if (mBroadcaster != null) { + mBroadcaster.stop(); + mBroadcaster = null; + } + } + + @Override + public void onConfigChange() { + if (mBroadcaster != null) { + mBroadcaster.onConfigChange(); + } + } + + @Override + public void setMute(boolean mute) { + if (mBroadcaster != null) { + mBroadcaster.setMute(mute); + } + } + + /** + * Broadcaster のインスタンスを作成します. + * + * @param broadcastURI 配信先の URI + * @return Broadcaster のインスタンス + */ + public abstract Broadcaster createBroadcaster(String broadcastURI); + + private void postBroadcastStarted(Broadcaster broadcaster) { + if (mOnEventListener != null) { + mOnEventListener.onStarted(broadcaster); + } + } + + private void postBroadcastStopped(Broadcaster broadcaster) { + if (mOnEventListener != null) { + mOnEventListener.onStopped(broadcaster); + } + } + + private void postBroadcastError(Broadcaster broadcaster, Exception e) { + if (mOnEventListener != null) { + mOnEventListener.onError(broadcaster, e); + } + } + + /** + * Notification の Id を取得します. + * + * @return Notification の Id + */ + private int getNotificationId() { + return 1000 + mRecorder.getId().hashCode(); + } + + /** + * プレビュー配信サーバ停止用の Notification を削除します. + * + * @param id notification を識別する ID + */ + private void hideNotification(String id) { + NotificationManager manager = (NotificationManager) mContext + .getSystemService(Service.NOTIFICATION_SERVICE); + if (manager != null) { + manager.cancel(id, getNotificationId()); + } + } + + /** + * プレビュー配信サーバ停止用の Notification を送信します. + * + * @param id notification を識別する ID + * @param name 名前 + */ + private void sendNotification(String id, String name) { + PendingIntent contentIntent = createPendingIntent(id); + Notification notification = createNotification(contentIntent, null, name); + NotificationManager manager = (NotificationManager) mContext + .getSystemService(Service.NOTIFICATION_SERVICE); + if (manager != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String channelId = mContext.getResources().getString(R.string.uvc_notification_preview_channel_id); + NotificationChannel channel = new NotificationChannel( + channelId, + mContext.getResources().getString(R.string.uvc_notification_recorder_broadcast), + NotificationManager.IMPORTANCE_LOW); + channel.setDescription(mContext.getResources().getString(R.string.uvc_notification_recorder_broadcast_content)); + manager.createNotificationChannel(channel); + notification = createNotification(contentIntent, channelId, name); + } + manager.notify(id, getNotificationId(), notification); + } + } + + /** + * Notificationを作成する. + * + * @param pendingIntent Notificationがクリックされたときに起動する Intent + * @param channelId チャンネルID + * @param name 名前 + * @return Notification + */ + private Notification createNotification(final PendingIntent pendingIntent, final String channelId, String name) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext.getApplicationContext()); + builder.setContentIntent(pendingIntent); + builder.setTicker(mContext.getString(R.string.uvc_notification_recorder_broadcast_ticker)); + builder.setSmallIcon(R.drawable.dconnect_icon); + builder.setContentTitle(mContext.getString(R.string.uvc_notification_recorder_broadcast, name)); + builder.setContentText(mContext.getString(R.string.uvc_notification_recorder_broadcast_content)); + builder.setWhen(System.currentTimeMillis()); + builder.setAutoCancel(true); + builder.setOngoing(true); + return builder.build(); + } else { + Notification.Builder builder = new Notification.Builder(mContext.getApplicationContext()); + builder.setContentIntent(pendingIntent); + builder.setTicker(mContext.getString(R.string.uvc_notification_preview_ticker)); + int iconType = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ? + R.drawable.dconnect_icon : R.drawable.dconnect_icon_lollipop; + builder.setSmallIcon(iconType); + builder.setContentTitle(mContext.getString(R.string.uvc_notification_recorder_broadcast, name)); + builder.setContentText(mContext.getString(R.string.uvc_notification_recorder_broadcast_content)); + builder.setWhen(System.currentTimeMillis()); + builder.setAutoCancel(true); + builder.setOngoing(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && channelId != null) { + builder.setChannelId(channelId); + } + return builder.build(); + } + } + + /** + * PendingIntent を作成する. + * + * @param id レコーダ ID + * + * @return PendingIntent + */ + private PendingIntent createPendingIntent(String id) { + Intent intent = new Intent(); + intent.setAction(MediaRecorderManager.ACTION_STOP_BROADCAST); + intent.putExtra(MediaRecorderManager.KEY_RECORDER_ID, id); + return PendingIntent.getBroadcast(mContext, getNotificationId(), intent, 0); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractBroadcaster.java new file mode 100644 index 0000000000..8300311a08 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractBroadcaster.java @@ -0,0 +1,117 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import android.graphics.Rect; +import android.util.Size; + +import org.deviceconnect.android.libmedia.streaming.audio.AudioQuality; +import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; +import org.deviceconnect.android.libmedia.streaming.video.VideoQuality; + +public abstract class AbstractBroadcaster implements Broadcaster { + /** + * 配信先の URI. + */ + private final String mBroadcastURI; + + /** + * カメラを操作するレコーダ. + */ + private final MediaRecorder mRecorder; + + public AbstractBroadcaster(MediaRecorder recorder, String broadcastURI) { + mRecorder = recorder; + mBroadcastURI = broadcastURI; + } + + @Override + public String getMimeType() { + return ""; + } + + @Override + public String getBroadcastURI() { + return mBroadcastURI; + } + + @Override + public void onConfigChange() { + VideoQuality videoQuality = getVideoQuality(); + if (videoQuality != null) { + setVideoQuality(videoQuality); + } + + AudioQuality audioQuality = getAudioQuality(); + if (audioQuality != null) { + setAudioQuality(audioQuality); + + MediaRecorder.Settings settings = getRecorder().getSettings(); + setMute(settings.isMute()); + } + } + + /** + * 映像の設定を取得します. + * + * 映像が使用されていない場合は null を返却すること。 + * + * @return 映像の設定 + */ + protected VideoQuality getVideoQuality() { + return null; + } + + /** + * 音声の設定を取得します. + * + * 音声が使用されていない場合は null を返却すること。 + * + * @return 音声の設定 + */ + protected AudioQuality getAudioQuality() { + return null; + } + + /** + * Broadcaster で使用するレコーダを取得します. + * + * @return Broadcaster で使用するレコーダ + */ + public MediaRecorder getRecorder() { + return mRecorder; + } + + /** + * VideoEncoder の設定に、MediaRecorder の設定を反映します. + * + * @param videoQuality 設定を行う VideoEncoder の VideoQuality + */ + public void setVideoQuality(VideoQuality videoQuality) { + MediaRecorder recorder = getRecorder(); + MediaRecorder.Settings settings = recorder.getSettings(); + + Rect rect = settings.getDrawingRange(); + if (rect != null) { + videoQuality.setVideoWidth(rect.width()); + videoQuality.setVideoHeight(rect.height()); + } else { + Size previewSize = settings.getPreviewSize(); + videoQuality.setVideoWidth(previewSize.getWidth()); + videoQuality.setVideoHeight(previewSize.getHeight()); + } + videoQuality.setBitRate(settings.getPreviewBitRate()); + videoQuality.setFrameRate(settings.getPreviewMaxFrameRate()); + videoQuality.setIFrameInterval(settings.getPreviewKeyFrameInterval()); + videoQuality.setUseSoftwareEncoder(settings.isUseSoftwareEncoder()); + videoQuality.setIntraRefresh(settings.getIntraRefresh()); + videoQuality.setProfile(settings.getProfile()); + videoQuality.setLevel(settings.getLevel()); + } + + /** + * AudioEncoder の設定に、MediaRecorder の設定を反映します. + * + * @param audioQuality 設定を行う AudioEncoder の AudioQuality + */ + public void setAudioQuality(AudioQuality audioQuality) { + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractMJPEGPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractMJPEGPreviewServer.java new file mode 100644 index 0000000000..f732132a9e --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractMJPEGPreviewServer.java @@ -0,0 +1,195 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; +import android.util.Size; + +import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; +import org.deviceconnect.android.libmedia.streaming.mjpeg.MJPEGEncoder; +import org.deviceconnect.android.libmedia.streaming.mjpeg.MJPEGQuality; +import org.deviceconnect.android.libmedia.streaming.mjpeg.MJPEGServer; + +import java.io.IOException; +import java.net.Socket; + +import javax.net.ssl.SSLContext; + +public abstract class AbstractMJPEGPreviewServer extends AbstractPreviewServer { + /** + * Motion JPEG のマイムタイプを定義します. + */ + protected static final String MIME_TYPE = "video/x-mjpeg"; + + /** + * サーバー名を定義します. + */ + private static final String SERVER_NAME = "Android Host Camera2 MJPEG Server"; + + /** + * MotionJPEG 配信サーバ. + */ + private MJPEGServer mMJPEGServer; + + public AbstractMJPEGPreviewServer(Context context, MediaRecorder recorder, boolean useSSL) { + super(context, recorder, useSSL); + } + + // PreviewServer + + @Override + public String getUri() { + return mMJPEGServer == null ? null : mMJPEGServer.getUri(); + } + + @Override + public String getMimeType() { + return MIME_TYPE; + } + + @Override + public void startWebServer(final OnWebServerStartCallback callback) { + if (mMJPEGServer == null) { + SSLContext sslContext = getSSLContext(); + if (useSSLContext() && sslContext == null) { + callback.onFail(); + return; + } + + mMJPEGServer = new MJPEGServer(); + mMJPEGServer.setServerName(SERVER_NAME); + mMJPEGServer.setServerPort(getPort()); + mMJPEGServer.setCallback(mCallback); + if (useSSLContext()) { + mMJPEGServer.setSSLContext(sslContext); + } + try { + mMJPEGServer.start(); + } catch (Exception e) { + callback.onFail(); + return; + } + } + callback.onStart(getUri()); + } + + @Override + public void stopWebServer() { + if (mMJPEGServer != null) { + mMJPEGServer.stop(); + mMJPEGServer = null; + } + } + + @Override + public boolean requestSyncFrame() { + // 何もしない + return false; + } + + @Override + public long getBPS() { + return mMJPEGServer != null ? mMJPEGServer.getBPS() : 0; + } + + @Override + public void onConfigChange() { + setEncoderQuality(); + restartEncoder(); + } + + /** + * エンコーダの設定を行います. + */ + private void setEncoderQuality() { + if (mMJPEGServer != null) { + MJPEGEncoder encoder = mMJPEGServer.getMJPEGEncoder(); + if (encoder != null) { + setMJPEGQuality(encoder.getMJPEGQuality()); + } + } + } + + /** + * エンコーダを再スタートさせて、設定を反映します. + */ + private void restartEncoder() { + if (mMJPEGServer != null) { + new Thread(() -> { + if (mMJPEGServer != null) { + mMJPEGServer.restartEncoder(); + } + }).start(); + } + } + + /** + * MJPEG の設定を行います. + * + * @param quality 設定を行う MJPEGQuality + */ + private void setMJPEGQuality(MJPEGQuality quality) { + MediaRecorder recorder = getRecorder(); + MediaRecorder.Settings settings = recorder.getSettings(); + + Rect rect = settings.getDrawingRange(); + if (rect != null) { + quality.setWidth(rect.width()); + quality.setHeight(rect.height()); + } else { + Size previewSize = settings.getPreviewSize(); + quality.setWidth(previewSize.getWidth()); + quality.setHeight(previewSize.getHeight()); + } + quality.setFrameRate(settings.getPreviewMaxFrameRate()); + quality.setQuality(settings.getPreviewQuality()); + } + + /** + * MJPEG 用のエンコーダを作成します. + * + * @return MJPEG 用のエンコーダ + */ + protected abstract MJPEGEncoder createSurfaceMJPEGEncoder(); + + /** + * MJPEGServer からのイベントを受け取るためのコールバック. + */ + private final MJPEGServer.Callback mCallback = new MJPEGServer.Callback() { + @Override + public boolean onAccept(Socket socket) { + if (DEBUG) { + Log.d(TAG, "MJPEGServer.Callback#onAccept: "); + Log.d(TAG, " socket: " + socket); + } + // 特に制限を付けないので、常に true を返却 + return true; + } + + @Override + public void onClosed(Socket socket) { + if (DEBUG) { + Log.d(TAG, "MJPEGServer.Callback#onClosed: "); + Log.d(TAG, " socket: " + socket); + } + } + + @Override + public MJPEGEncoder createMJPEGEncoder() { + if (DEBUG) { + Log.d(TAG, "MJPEGServer.Callback#createMJPEGEncoder: "); + } + + MJPEGEncoder encoder = createSurfaceMJPEGEncoder(); + setMJPEGQuality(encoder.getMJPEGQuality()); + return encoder; + } + + @Override + public void releaseMJPEGEncoder(MJPEGEncoder encoder) { + if (DEBUG) { + Log.d(TAG, "MJPEGServer.Callback#releaseMJPEGEncoder: "); + } + } + }; +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractMediaRecorder.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractMediaRecorder.java new file mode 100644 index 0000000000..417772cb11 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractMediaRecorder.java @@ -0,0 +1,463 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; + +import org.deviceconnect.android.activity.PermissionUtility; +import org.deviceconnect.android.deviceplugin.uvc.R; +import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; + +import java.util.ArrayList; +import java.util.List; + +import javax.net.ssl.SSLContext; + +public abstract class AbstractMediaRecorder implements MediaRecorder { + /** + * コンテキスト. + */ + private final Context mContext; + + /** + * リクエストの処理を実行するハンドラ. + */ + private final Handler mRequestHandler; + + /** + * イベント通知用リスナー. + */ + private OnEventListener mOnEventListener; + + /** + * レコーダの状態. + */ + private State mState = State.INACTIVE; + + /** + * コンストラクタ. + */ + public AbstractMediaRecorder(Context context) { + if (context == null) { + throw new IllegalArgumentException("context is null."); + } + + mContext = context; + + HandlerThread requestThread = new HandlerThread("uvc-media-recorder"); + requestThread.start(); + mRequestHandler = new Handler(requestThread.getLooper()); + } + + // Implements MediaRecorder methods. + + @Override + public void initialize() { + BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); + if (broadcasterProvider != null) { + broadcasterProvider.setOnEventListener(new BroadcasterProvider.OnEventListener() { + @Override + public void onStarted(Broadcaster broadcaster) { + postOnBroadcasterStarted(broadcaster); + } + + @Override + public void onStopped(Broadcaster broadcaster) { + postOnBroadcasterStopped(broadcaster); + } + + @Override + public void onError(Broadcaster broadcaster, Exception e) { + postOnBroadcasterError(broadcaster, e); + } + }); + } + + PreviewServerProvider previewProvider = getServerProvider(); + if (previewProvider != null) { + previewProvider.setOnEventListener(new PreviewServerProvider.OnEventListener() { + @Override + public void onStarted(List servers) { + postOnPreviewStarted(servers); + } + + @Override + public void onStopped() { + postOnPreviewStopped(); + } + + @Override + public void onError(PreviewServer server, Exception e) { + postOnPreviewError(e); + } + }); + } + } + + @Override + public void clean() { + try { + BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); + if (broadcasterProvider != null) { + broadcasterProvider.stopBroadcaster(); + } + + PreviewServerProvider serverProvider = getServerProvider(); + if (serverProvider != null) { + serverProvider.stopServers();; + } + + EGLSurfaceDrawingThread thread = getSurfaceDrawingThread(); + if (thread != null) { + thread.stop(); + } + } catch (Exception e) { + // ignore. + } + } + + @Override + public void destroy() { + clean(); + mRequestHandler.getLooper().quit(); + } + + @Override + public void onConfigChange() { + PreviewServerProvider serverProvider = getServerProvider(); + if (serverProvider != null) { + serverProvider.onConfigChange(); + } + + BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); + if (broadcasterProvider != null) { + broadcasterProvider.onConfigChange(); + } + + postOnConfigChanged(); + } + + @Override + public State getState() { + return mState; + } + + @Override + public boolean isPreviewRunning() { + PreviewServerProvider provider = getServerProvider(); + return provider != null && provider.isRunning(); + } + + @Override + public List startPreview() { + PreviewServerProvider provider = getServerProvider(); + if (provider == null) { + return new ArrayList<>(); + } + + List servers = provider.startServers(); + if (!servers.isEmpty()) { + provider.setMute(getSettings().isMute()); + } + return servers; + } + + @Override + public void stopPreview() { + PreviewServerProvider provider = getServerProvider(); + if (provider != null) { + provider.stopServers(); + } + } + + @Override + public boolean isBroadcasterRunning() { + BroadcasterProvider provider = getBroadcasterProvider(); + return provider != null && provider.isRunning(); + } + + @Override + public Broadcaster startBroadcaster(String uri) { + if (uri == null) { + return null; + } + + BroadcasterProvider provider = getBroadcasterProvider(); + if (provider == null) { + return null; + } + + Broadcaster broadcaster = provider.startBroadcaster(uri); + if (broadcaster != null) { + broadcaster.setMute(getSettings().isMute()); + } + return broadcaster; + } + + @Override + public void stopBroadcaster() { + BroadcasterProvider provider = getBroadcasterProvider(); + if (provider != null) { + provider.stopBroadcaster(); + } + } + + @Override + public void setMute(boolean mute) { + Settings settings = getSettings(); + settings.setMute(mute); + + PreviewServerProvider previewProvider = getServerProvider(); + if (previewProvider != null) { + previewProvider.setMute(mute); + } + + BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); + if (broadcasterProvider != null) { + broadcasterProvider.setMute(mute); + } + } + + @Override + public boolean isMute() { + return getSettings().isMute(); + } + + @Override + public void setSSLContext(SSLContext sslContext) { + PreviewServerProvider previewProvider = getServerProvider(); + if (previewProvider != null) { + for (PreviewServer server : previewProvider.getServers()) { + server.setSSLContext(sslContext); + } + } + } + + @Override + public void setOnEventListener(OnEventListener listener) { + mOnEventListener = listener; + } + + @Override + public long getBPS() { + PreviewServerProvider previewProvider = getServerProvider(); + if (previewProvider == null) { + return 0; + } + + long bps = 0; + List servers = previewProvider.getServers(); + for (PreviewServer previewServer : servers) { + bps += previewServer.getBPS(); + } + return bps; + } + + /** + * Runnable を順番に実行します. + * + * @param run 実行する Runnable + */ + protected void postRequestHandler(Runnable run) { + mRequestHandler.post(run); + } + + /** + * レコーダの状態を設定します. + * + * @param state レコーダの状態 + */ + protected void setState(State state) { + mState = state; + } + + /** + * コンテキストを取得します. + * + * @return コンテキスト + */ + public Context getContext() { + return mContext; + } + + /** + * パーミッション要求します. + * + * @param permissions パーミッションのリスト + * @param callback パーミションの結果を受け取るコールバック + */ + protected void requestPermission(String[] permissions, PermissionCallback callback) { + Handler handler = new Handler(Looper.getMainLooper()); + PermissionUtility.requestPermissions(getContext(), handler, permissions, new PermissionUtility.PermissionRequestCallback() { + @Override + public void onSuccess() { + callback.onAllowed(); + } + + @Override + public void onFail(@NonNull String deniedPermission) { + callback.onDisallowed(); + } + }); + } + + /** + * NotificationId を取得します. + * + * @return NotificationId + */ + private int getNotificationId() { + return 2000 + getId().hashCode(); + } + + /** + * レコーディング停止用の Notification を削除します. + * + * @param id notification を識別する ID + */ + private void hideNotification(String id) { + NotificationManager manager = (NotificationManager) mContext + .getSystemService(Service.NOTIFICATION_SERVICE); + if (manager != null) { + manager.cancel(id, getNotificationId()); + } + } + + /** + * 録音・録画停止用の Notification を表示します. + * + * @param id notification を識別する ID + * @param name 名前 + */ + private void showNotificationForStopRecording(String id, String name) { + PendingIntent contentIntent = createPendingIntentForStopRecording(id); + Notification notification = createNotificationForStopRecording(contentIntent, null, name); + NotificationManager manager = (NotificationManager) mContext + .getSystemService(Service.NOTIFICATION_SERVICE); + if (manager != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String channelId = mContext.getResources().getString(R.string.uvc_notification_preview_channel_id); + NotificationChannel channel = new NotificationChannel( + channelId, + mContext.getResources().getString(R.string.uvc_notification_recorder_recording), + NotificationManager.IMPORTANCE_LOW); + channel.setDescription(mContext.getResources().getString(R.string.uvc_notification_recorder_recording_content)); + manager.createNotificationChannel(channel); + notification = createNotificationForStopRecording(contentIntent, channelId, name); + } + manager.notify(id, getNotificationId(), notification); + } + } + + /** + * 録音・録画停止用の Notification を作成する. + * + * @param pendingIntent Notificationがクリックされたときに起動する Intent + * @param channelId チャンネルID + * @param name 名前 + * @return Notification + */ + protected Notification createNotificationForStopRecording(final PendingIntent pendingIntent, final String channelId, String name) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext.getApplicationContext()); + builder.setContentIntent(pendingIntent); + builder.setTicker(mContext.getString(R.string.uvc_notification_recorder_recording_ticker)); + builder.setSmallIcon(R.drawable.dconnect_icon); + builder.setContentTitle(mContext.getString(R.string.uvc_notification_recorder_recording, name)); + builder.setContentText(mContext.getString(R.string.uvc_notification_recorder_recording_content)); + builder.setWhen(System.currentTimeMillis()); + builder.setAutoCancel(true); + builder.setOngoing(true); + return builder.build(); + } else { + Notification.Builder builder = new Notification.Builder(mContext.getApplicationContext()); + builder.setContentIntent(pendingIntent); + builder.setTicker(mContext.getString(R.string.uvc_notification_preview_ticker)); + int iconType = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ? + R.drawable.dconnect_icon : R.drawable.dconnect_icon_lollipop; + builder.setSmallIcon(iconType); + builder.setContentTitle(mContext.getString(R.string.uvc_notification_recorder_recording, name)); + builder.setContentText(mContext.getString(R.string.uvc_notification_recorder_recording_content)); + builder.setWhen(System.currentTimeMillis()); + builder.setAutoCancel(true); + builder.setOngoing(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && channelId != null) { + builder.setChannelId(channelId); + } + return builder.build(); + } + } + + /** + * PendingIntent を作成する. + * + * @param id カメラ ID + * + * @return PendingIntent + */ + private PendingIntent createPendingIntentForStopRecording(String id) { + Intent intent = new Intent(); + intent.setAction(MediaRecorderManager.ACTION_STOP_RECORDING); + intent.putExtra(MediaRecorderManager.KEY_RECORDER_ID, id); + return PendingIntent.getBroadcast(mContext, getNotificationId(), intent, 0); + } + + protected void postOnConfigChanged() { + if (mOnEventListener != null) { + mOnEventListener.onConfigChanged(); + } + } + + protected void postOnPreviewStarted(List servers) { + if (mOnEventListener != null) { + mOnEventListener.onPreviewStarted(servers); + } + } + + protected void postOnPreviewStopped() { + if (mOnEventListener != null) { + mOnEventListener.onPreviewStopped(); + } + } + + protected void postOnPreviewError(Exception e) { + if (mOnEventListener != null) { + mOnEventListener.onPreviewError(e); + } + } + + protected void postOnBroadcasterStarted(Broadcaster broadcaster) { + if (mOnEventListener != null) { + mOnEventListener.onBroadcasterStarted(broadcaster); + } + } + + protected void postOnBroadcasterStopped(Broadcaster broadcaster) { + if (mOnEventListener != null) { + mOnEventListener.onBroadcasterStopped(broadcaster); + } + } + + protected void postOnBroadcasterError(Broadcaster broadcaster, Exception e) { + if (mOnEventListener != null) { + mOnEventListener.onBroadcasterError(broadcaster, e); + } + } + + protected void postOnError(Exception e) { + if (mOnEventListener != null) { + mOnEventListener.onError(e); + } + } +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractPreviewServer.java new file mode 100644 index 0000000000..ef79f607d1 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractPreviewServer.java @@ -0,0 +1,212 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import android.content.Context; +import android.graphics.Rect; +import android.util.Size; + +import org.deviceconnect.android.deviceplugin.uvc.BuildConfig; +import org.deviceconnect.android.libmedia.streaming.audio.AudioQuality; +import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; +import org.deviceconnect.android.libmedia.streaming.video.VideoQuality; + +import javax.net.ssl.SSLContext; + +/** + * プレビュー配信サーバ. + */ +public abstract class AbstractPreviewServer implements PreviewServer { + protected static final boolean DEBUG = BuildConfig.DEBUG; + protected static final String TAG = "host.dplugin"; + + /** + * コンテキスト. + */ + private final Context mContext; + + /** + * プレビュー再生を行うレコーダ. + */ + private final MediaRecorder mMediaRecorder; + + /** + * プレビュー配信サーバのポート番号. + */ + private int mPort; + + /** + * ミュート設定. + */ + private boolean mMute; + + /** + * SSLContext のインスタンス. + */ + private SSLContext mSSLContext; + + /** + * SSL の使用フラグ. + */ + private final boolean mUseSSL; + + /** + * コンストラクタ. + * + *

+ * デフォルトでは、mute は true に設定しています。 + * デフォルトでは、mUseSSL は false に設定します。 + *

+ * + * @param context コンテキスト + * @param recorder プレビューで表示するレコーダ + */ + public AbstractPreviewServer(Context context, MediaRecorder recorder) { + this(context, recorder, false); + } + + /** + * コンストラクタ. + * + *

+ * デフォルトでは、mute は true に設定しています。 + *

+ * + * @param context コンテキスト + * @param recorder プレビューで表示するレコーダ + * @param useSSL SSL使用フラグ + */ + public AbstractPreviewServer(Context context, MediaRecorder recorder, boolean useSSL) { + mContext = context; + mMediaRecorder = recorder; + mUseSSL = useSSL; + mMute = true; + } + + // Implements PreviewServer methods. + + @Override + public int getPort() { + return mPort; + } + + @Override + public void setPort(int port) { + mPort = port; + } + + @Override + public void onConfigChange() { + VideoQuality videoQuality = getVideoQuality(); + if (videoQuality != null) { + setVideoQuality(videoQuality); + } + + AudioQuality audioQuality = getAudioQuality(); + if (audioQuality != null) { + setAudioQuality(audioQuality); + + MediaRecorder.Settings settings = getRecorder().getSettings(); + setMute(settings.isMute()); + } + } + + @Override + public void setMute(boolean mute) { + mMute = mute; + } + + @Override + public boolean isMuted() { + return mMute; + } + + @Override + public boolean useSSLContext() { + return mUseSSL; + } + + @Override + public void setSSLContext(final SSLContext sslContext) { + mSSLContext = sslContext; + } + + @Override + public SSLContext getSSLContext() { + return mSSLContext; + } + + /** + * コンテキストを取得します. + * + * @return コンテキスト + */ + public Context getContext() { + return mContext; + } + + /** + * プレビューを表示するレコーダー. + * + * @return レコーダー + */ + public MediaRecorder getRecorder() { + return mMediaRecorder; + } + + /** + * 映像の設定を取得します. + * + * 映像が使用されていない場合は null を返却すること。 + * + * @return 映像の設定 + */ + protected VideoQuality getVideoQuality() { + return null; + } + + /** + * 音声の設定を取得します. + * + * 音声が使用されていない場合は null を返却すること。 + * + * @return 音声の設定 + */ + protected AudioQuality getAudioQuality() { + return null; + } + + /** + * VideoEncoder の設定に、MediaRecorder の設定を反映します. + * + * @param videoQuality 設定を行う VideoEncoder の VideoQuality + */ + public void setVideoQuality(VideoQuality videoQuality) { + MediaRecorder recorder = getRecorder(); + MediaRecorder.Settings settings = recorder.getSettings(); + + Rect rect = settings.getDrawingRange(); + if (rect != null) { + videoQuality.setVideoWidth(rect.width()); + videoQuality.setVideoHeight(rect.height()); + } else { + Size previewSize = settings.getPreviewSize(); + videoQuality.setVideoWidth(previewSize.getWidth()); + videoQuality.setVideoHeight(previewSize.getHeight()); + } + videoQuality.setBitRate(settings.getPreviewBitRate()); + videoQuality.setFrameRate(settings.getPreviewMaxFrameRate()); + videoQuality.setIFrameInterval(settings.getPreviewKeyFrameInterval()); + videoQuality.setUseSoftwareEncoder(settings.isUseSoftwareEncoder()); + videoQuality.setIntraRefresh(settings.getIntraRefresh()); + videoQuality.setProfile(settings.getProfile()); + videoQuality.setLevel(settings.getLevel()); + } + + /** + * AudioEncoder の設定に、MediaRecorder の設定を反映します. + * + * @param audioQuality 設定を行う AudioEncoder の AudioQuality + */ + public void setAudioQuality(AudioQuality audioQuality) { + + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractPreviewServerProvider.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractPreviewServerProvider.java new file mode 100644 index 0000000000..59819bf46d --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractPreviewServerProvider.java @@ -0,0 +1,300 @@ +/* + HostDevicePreviewServer.java + Copyright (c) 2014 NTT DOCOMO,INC. + Released under the MIT license + http://opensource.org/licenses/mit-license.php + */ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; + +import org.deviceconnect.android.deviceplugin.uvc.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Host Device Preview Server. + * + * @author NTT DOCOMO, INC. + */ +public abstract class AbstractPreviewServerProvider implements PreviewServerProvider { + /** + * コンテキスト. + */ + private final Context mContext; + + /** + * プレビュー配信サーバーのリスト. + */ + private final List mPreviewServers = new ArrayList<>(); + + /** + * プレビュー配信を行うレコーダ. + */ + private final MediaRecorder mRecorder; + + /** + * Notification 表示フラグ. + */ + private boolean mIsRunning; + + /** + * プレビュー配信サーバのイベントを通知するリスナー. + */ + private OnEventListener mOnEventListener; + + /** + * コンストラクタ. + * @param context コンテキスト + */ + public AbstractPreviewServerProvider(final Context context, final MediaRecorder recorder) { + mContext = context; + mRecorder = recorder; + mIsRunning = false; + } + + // PreviewServerProvider + + @Override + public List getSupportedMimeType() { + List mimeType = new ArrayList<>(); + for (PreviewServer server : getServers()) { + mimeType.add(server.getMimeType()); + } + return mimeType; + } + + @Override + public void addServer(PreviewServer server) { + mPreviewServers.add(server); + } + + @Override + public List getServers() { + return mPreviewServers; + } + + @Override + public PreviewServer getServerByMimeType(String mimeType) { + for (PreviewServer server : getServers()) { + if (server.getMimeType().equalsIgnoreCase(mimeType)) { + return server; + } + } + return null; + } + + @Override + public boolean isRunning() { + return mIsRunning; + } + + @Override + public List startServers() { + List results = new ArrayList<>(); + + CountDownLatch latch = new CountDownLatch(mPreviewServers.size()); + for (PreviewServer server : mPreviewServers) { + server.startWebServer(new PreviewServer.OnWebServerStartCallback() { + @Override + public void onStart(@NonNull String uri) { + results.add(server); + latch.countDown(); + } + + @Override + public void onFail() { + latch.countDown(); + } + }); + } + + try { + latch.await(5, TimeUnit.SECONDS); + if (results.size() > 0) { + mIsRunning = true; + sendNotification(mRecorder.getId(), mRecorder.getName()); + postPreviewStarted(results); + } + } catch (InterruptedException e) { + // ignore. + } + return results; + } + + @Override + public void stopServers() { + hideNotification(mRecorder.getId()); + + for (PreviewServer server : getServers()) { + server.stopWebServer(); + } + + if (mIsRunning) { + mIsRunning = false; + postPreviewStopped(); + } + } + + @Override + public List requestSyncFrame() { + List result = new ArrayList<>(); + for (PreviewServer server : getServers()) { + if (server.requestSyncFrame()) { + result.add(server); + } + } + return result; + } + + @Override + public void onConfigChange() { + for (PreviewServer server : getServers()) { + server.onConfigChange(); + } + } + + @Override + public void setMute(boolean mute) { + for (PreviewServer server : getServers()) { + server.setMute(mute); + } + } + + @Override + public void setOnEventListener(OnEventListener listener) { + mOnEventListener = listener; + } + + protected void postPreviewStarted(List servers) { + if (mOnEventListener != null) { + mOnEventListener.onStarted(servers); + } + } + + protected void postPreviewStopped() { + if (mOnEventListener != null) { + mOnEventListener.onStopped(); + } + } + + protected void postPreviewError(PreviewServer server, Exception e) { + if (mOnEventListener != null) { + mOnEventListener.onError(server, e); + } + } + + /** + * Notification の Id を取得します. + * + * @return Notification の Id + */ + protected int getNotificationId() { + return 100 + mRecorder.getId().hashCode(); + } + + /** + * プレビュー配信サーバ停止用の Notification を削除します. + * + * @param id notification を識別する ID + */ + private void hideNotification(String id) { + NotificationManager manager = (NotificationManager) mContext + .getSystemService(Service.NOTIFICATION_SERVICE); + if (manager != null) { + manager.cancel(id, getNotificationId()); + } + } + + /** + * プレビュー配信サーバ停止用の Notification を送信します. + * + * @param id notification を識別する ID + * @param name 名前 + */ + private void sendNotification(String id, String name) { + PendingIntent contentIntent = createPendingIntent(id); + Notification notification = createNotification(contentIntent, null, name); + NotificationManager manager = (NotificationManager) mContext + .getSystemService(Service.NOTIFICATION_SERVICE); + if (manager != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String channelId = mContext.getResources().getString(R.string.uvc_notification_preview_channel_id); + NotificationChannel channel = new NotificationChannel( + channelId, + mContext.getResources().getString(R.string.uvc_notification_recorder_preview), + NotificationManager.IMPORTANCE_LOW); + channel.setDescription(mContext.getResources().getString(R.string.uvc_notification_recorder_preview_content)); + manager.createNotificationChannel(channel); + notification = createNotification(contentIntent, channelId, name); + } + manager.notify(id, getNotificationId(), notification); + } + } + + /** + * Notificationを作成する. + * + * @param pendingIntent Notificationがクリックされたときに起動する Intent + * @param channelId チャンネルID + * @param name 名前 + * @return Notification + */ + protected Notification createNotification(final PendingIntent pendingIntent, final String channelId, String name) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext.getApplicationContext()); + builder.setContentIntent(pendingIntent); + builder.setTicker(mContext.getString(R.string.uvc_notification_recorder_preview_ticker)); + builder.setSmallIcon(R.drawable.dconnect_icon); + builder.setContentTitle(mContext.getString(R.string.uvc_notification_recorder_preview, name)); + builder.setContentText(mContext.getString(R.string.uvc_notification_recorder_preview_content)); + builder.setWhen(System.currentTimeMillis()); + builder.setAutoCancel(true); + builder.setOngoing(true); + return builder.build(); + } else { + Notification.Builder builder = new Notification.Builder(mContext.getApplicationContext()); + builder.setContentIntent(pendingIntent); + builder.setTicker(mContext.getString(R.string.uvc_notification_preview_ticker)); + int iconType = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ? + R.drawable.dconnect_icon : R.drawable.dconnect_icon_lollipop; + builder.setSmallIcon(iconType); + builder.setContentTitle(mContext.getString(R.string.uvc_notification_recorder_preview, name)); + builder.setContentText(mContext.getString(R.string.uvc_notification_recorder_preview_content)); + builder.setWhen(System.currentTimeMillis()); + builder.setAutoCancel(true); + builder.setOngoing(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && channelId != null) { + builder.setChannelId(channelId); + } + return builder.build(); + } + } + + /** + * PendingIntent を作成する. + * + * @param id レコーダ ID + * + * @return PendingIntent + */ + private PendingIntent createPendingIntent(String id) { + Intent intent = new Intent(); + intent.setAction(MediaRecorderManager.ACTION_STOP_PREVIEW); + intent.putExtra(MediaRecorderManager.KEY_RECORDER_ID, id); + return PendingIntent.getBroadcast(mContext, getNotificationId(), intent, 0); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractRTMPBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractRTMPBroadcaster.java new file mode 100644 index 0000000000..f615294be9 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractRTMPBroadcaster.java @@ -0,0 +1,177 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import android.util.Log; + +import org.deviceconnect.android.libmedia.streaming.MediaEncoderException; +import org.deviceconnect.android.libmedia.streaming.audio.AudioEncoder; +import org.deviceconnect.android.libmedia.streaming.audio.AudioQuality; +import org.deviceconnect.android.libmedia.streaming.audio.MicAACLATMEncoder; +import org.deviceconnect.android.libmedia.streaming.rtmp.RtmpClient; +import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; +import org.deviceconnect.android.libmedia.streaming.video.VideoQuality; + +public abstract class AbstractRTMPBroadcaster extends AbstractBroadcaster { + + /** + * RTMP 配信クライアント. + */ + private RtmpClient mRtmpClient; + + /** + * イベントを通知するためのリスナー. + */ + private OnEventListener mOnBroadcasterEventListener; + + public AbstractRTMPBroadcaster(MediaRecorder recorder, String broadcastURI) { + super(recorder, broadcastURI); + } + + /** + * RTMP で配信するための映像用エンコーダを取得します. + * + * @return RTMP で配信するための映像用エンコーダ + */ + protected VideoEncoder createVideoEncoder() { + return null; + } + + /** + * RTMP で配信するための音声用エンコーダを取得します. + * + * @return RTMP で配信するための音声用エンコーダ + */ + protected AudioEncoder createAudioEncoder() { + MediaRecorder.Settings settings = getRecorder().getSettings(); + if (settings.isAudioEnabled()) { + return new MicAACLATMEncoder(); + } + return null; + } + + @Override + public void setOnEventListener(OnEventListener listener) { + mOnBroadcasterEventListener = listener; + } + + @Override + public boolean isRunning() { + return mRtmpClient != null && mRtmpClient.isRunning(); + } + + @Override + public void start(OnStartCallback callback) { + VideoEncoder videoEncoder = createVideoEncoder(); + if (videoEncoder != null) { + setVideoQuality(videoEncoder.getVideoQuality()); + } + + AudioEncoder audioEncoder = createAudioEncoder(); + if (audioEncoder != null) { + setAudioQuality(audioEncoder.getAudioQuality()); + } + + MediaRecorder.Settings settings = getRecorder().getSettings(); + + mRtmpClient = new RtmpClient(getBroadcastURI()); + mRtmpClient.setMaxRetryCount(settings.getRetryCount()); + mRtmpClient.setRetryInterval(settings.getRetryInterval()); + mRtmpClient.setVideoEncoder(videoEncoder); + mRtmpClient.setAudioEncoder(audioEncoder); + mRtmpClient.setOnEventListener(new RtmpClient.OnEventListener() { + @Override + public void onStarted() { + if (callback != null) { + callback.onSuccess(); + } + + if (mOnBroadcasterEventListener != null) { + mOnBroadcasterEventListener.onStarted(); + } + } + + @Override + public void onStopped() { + if (mOnBroadcasterEventListener != null) { + mOnBroadcasterEventListener.onStopped(); + } + } + + @Override + public void onError(MediaEncoderException e) { + if (callback != null) { + callback.onFailed(e); + } + + if (mOnBroadcasterEventListener != null) { + mOnBroadcasterEventListener.onError(e); + } + + AbstractRTMPBroadcaster.this.stop(); + } + + @Override + public void onConnected() { + } + + @Override + public void onDisconnected() { + } + + @Override + public void onNewBitrate(long bitrate) { + } + }); + mRtmpClient.start(); + } + + @Override + public void stop() { + if (mRtmpClient != null) { + mRtmpClient.stop(); + mRtmpClient = null; + } + } + + @Override + public void setMute(boolean mute) { + if (mRtmpClient != null) { + mRtmpClient.setMute(mute); + } + } + + @Override + public boolean isMute() { + return mRtmpClient != null && mRtmpClient.isMute(); + } + + @Override + public void onConfigChange() { + super.onConfigChange(); + + if (mRtmpClient != null) { + mRtmpClient.restartVideoEncoder(); + } + } + + @Override + protected VideoQuality getVideoQuality() { + if (mRtmpClient != null) { + VideoEncoder videoEncoder = mRtmpClient.getVideoEncoder(); + if (videoEncoder != null) { + return videoEncoder.getVideoQuality(); + } + } + return null; + } + + @Override + protected AudioQuality getAudioQuality() { + if (mRtmpClient != null) { + AudioEncoder audioEncoder = mRtmpClient.getAudioEncoder(); + if (audioEncoder != null) { + return audioEncoder.getAudioQuality(); + } + } + return null; + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractRTSPPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractRTSPPreviewServer.java new file mode 100644 index 0000000000..bcbeb0b4bc --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractRTSPPreviewServer.java @@ -0,0 +1,219 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import android.content.Context; +import android.util.Log; + +import org.deviceconnect.android.libmedia.streaming.audio.AudioEncoder; +import org.deviceconnect.android.libmedia.streaming.audio.AudioQuality; +import org.deviceconnect.android.libmedia.streaming.rtsp.RtspServer; +import org.deviceconnect.android.libmedia.streaming.rtsp.session.RtspSession; +import org.deviceconnect.android.libmedia.streaming.rtsp.session.audio.AudioStream; +import org.deviceconnect.android.libmedia.streaming.rtsp.session.audio.MicAACLATMStream; +import org.deviceconnect.android.libmedia.streaming.rtsp.session.video.VideoStream; +import org.deviceconnect.android.libmedia.streaming.video.VideoQuality; + +import java.io.IOException; + +public abstract class AbstractRTSPPreviewServer extends AbstractPreviewServer { + + /** + * マイムタイプを定義します. + */ + private static final String MIME_TYPE = "video/x-rtp"; + + /** + * サーバー名を定義します. + */ + private static final String SERVER_NAME = "Android Host RTSP Server"; + + /** + * RTSP 配信サーバ.d + */ + private RtspServer mRtspServer; + + public AbstractRTSPPreviewServer(Context context, MediaRecorder recorder) { + this(context, recorder, false); + } + + public AbstractRTSPPreviewServer(Context context, MediaRecorder recorder, boolean useSSL) { + super(context, recorder, useSSL); + } + + @Override + public String getUri() { + return "rtsp://localhost:" + getPort(); + } + + @Override + public String getMimeType() { + return MIME_TYPE; + } + + @Override + public void startWebServer(final OnWebServerStartCallback callback) { + if (mRtspServer == null) { + mRtspServer = new RtspServer(); + mRtspServer.setServerName(SERVER_NAME); + mRtspServer.setServerPort(getPort()); + mRtspServer.setCallback(mCallback); + try { + mRtspServer.start(); + } catch (Exception e) { + callback.onFail(); + return; + } + } + callback.onStart(getUri()); + } + + @Override + public void stopWebServer() { + if (mRtspServer != null) { + mRtspServer.stop(); + mRtspServer = null; + } + } + + @Override + public boolean requestSyncFrame() { + RtspServer server = mRtspServer; + if (server != null) { + RtspSession session = server.getRtspSession(); + if (session != null) { + VideoStream videoStream = session.getVideoStream(); + if (videoStream != null) { + videoStream.getVideoEncoder().requestSyncKeyFrame(); + return true; + } + } + } + return false; + } + + @Override + public long getBPS() { + return mRtspServer != null ? mRtspServer.getBPS() : 0; + } + + @Override + public void onConfigChange() { + super.onConfigChange(); + restartVideoStream(); + } + + @Override + public void setMute(boolean mute) { + super.setMute(mute); + + if (mRtspServer != null) { + RtspSession session = mRtspServer.getRtspSession(); + if (session != null) { + AudioStream stream = session.getAudioStream(); + if (stream != null) { + stream.setMute(mute); + } + } + } + } + + @Override + protected VideoQuality getVideoQuality() { + if (mRtspServer != null) { + RtspSession session = mRtspServer.getRtspSession(); + if (session != null) { + VideoStream videoStream = session.getVideoStream(); + if (videoStream != null) { + return videoStream.getVideoEncoder().getVideoQuality(); + } + } + } + return null; + } + + @Override + protected AudioQuality getAudioQuality() { + if (mRtspServer != null) { + RtspSession session = mRtspServer.getRtspSession(); + if (session != null) { + AudioStream audioStream = session.getAudioStream(); + if (audioStream != null) { + return audioStream.getAudioEncoder().getAudioQuality(); + } + } + } + return null; + } + + /** + * エンコーダに設定を反映し、再スタートします. + */ + private void restartVideoStream() { + if (mRtspServer != null) { + RtspSession session = mRtspServer.getRtspSession(); + if (session != null) { + session.restartVideoStream(); + } + } + } + + /** + * 映像用の VideoStream を作成します. + * + * @return VideoStream のインスタンス + */ + protected VideoStream createVideoStream() { + return null; + } + + /** + * 音声用の AudioStream を作成します. + * + * @return AudioStream のインスタンス + */ + protected AudioStream createAudioStream() { + MediaRecorder recorder = getRecorder(); + MediaRecorder.Settings settings = recorder.getSettings(); + + if (settings.isAudioEnabled()) { + return new MicAACLATMStream(5004); + } + return null; + } + + /** + * RtspServer からのイベントを受け取るためのコールバック. + * + *

+ * RTSP 配信の開始時と停止時に呼び出されます。 + *

+ */ + private final RtspServer.Callback mCallback = new RtspServer.Callback() { + @Override + public void createSession(RtspSession session) { + if (DEBUG) { + Log.d(TAG, "RtspServer.Callback#createSession()"); + } + + VideoStream videoStream = createVideoStream(); + if (videoStream != null) { + setVideoQuality(videoStream.getVideoEncoder().getVideoQuality()); + session.setVideoMediaStream(videoStream); + } + + AudioStream audioStream = createAudioStream(); + if (audioStream != null) { + AudioEncoder audioEncoder = audioStream.getAudioEncoder(); + audioEncoder.setMute(isMuted()); + setAudioQuality(audioEncoder.getAudioQuality()); + session.setAudioMediaStream(audioStream); + } + } + + @Override + public void releaseSession(RtspSession session) { + if (DEBUG) { + Log.d(TAG, "RtspServer.Callback#releaseSession()"); + } + } + }; +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractSRTBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractSRTBroadcaster.java new file mode 100644 index 0000000000..b5bf3c7385 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractSRTBroadcaster.java @@ -0,0 +1,174 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import org.deviceconnect.android.libmedia.streaming.MediaEncoderException; +import org.deviceconnect.android.libmedia.streaming.audio.AudioEncoder; +import org.deviceconnect.android.libmedia.streaming.audio.AudioQuality; +import org.deviceconnect.android.libmedia.streaming.audio.MicAACLATMEncoder; +import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; +import org.deviceconnect.android.libmedia.streaming.video.VideoQuality; +import org.deviceconnect.android.libsrt.broadcast.SRTClient; + +public abstract class AbstractSRTBroadcaster extends AbstractBroadcaster { + + /** + * RTMP 配信クライアント. + */ + private SRTClient mSrtClient; + + /** + * イベントを通知するためのリスナー. + */ + private OnEventListener mOnBroadcasterEventListener; + + public AbstractSRTBroadcaster(MediaRecorder recorder, String broadcastURI) { + super(recorder, broadcastURI); + } + + /** + * SRT 配信に使用する VideoEncoder のインスタンスを作成します. + * + * @return SRT 配信に使用する VideoEncoder + */ + protected VideoEncoder createVideoEncoder() { + return null; + } + + /** + * SRT 配信に使用する AudioEncoder のインスタンスを作成します. + * + * @return SRT 配信に使用する AudioEncoder + */ + protected AudioEncoder createAudioEncoder() { + MediaRecorder.Settings settings = getRecorder().getSettings(); + if (settings.isAudioEnabled()) { + return new MicAACLATMEncoder(); + } + return null; + } + + @Override + public void setOnEventListener(OnEventListener listener) { + mOnBroadcasterEventListener = listener; + } + + @Override + public boolean isRunning() { + return mSrtClient != null && mSrtClient.isRunning(); + } + + @Override + public void start(OnStartCallback callback) { + VideoEncoder videoEncoder = createVideoEncoder(); + if (videoEncoder != null) { + setVideoQuality(videoEncoder.getVideoQuality()); + } + + AudioEncoder audioEncoder = createAudioEncoder(); + if (audioEncoder != null) { + setAudioQuality(audioEncoder.getAudioQuality()); + } + + MediaRecorder.Settings settings = getRecorder().getSettings(); + + mSrtClient = new SRTClient(getBroadcastURI()); + mSrtClient.setMaxRetryCount(settings.getRetryCount()); + mSrtClient.setRetryInterval(settings.getRetryInterval()); + mSrtClient.setVideoEncoder(videoEncoder); + mSrtClient.setAudioEncoder(audioEncoder); + mSrtClient.setOnEventListener(new SRTClient.OnEventListener() { + @Override + public void onStarted() { + if (callback != null) { + callback.onSuccess(); + } + + if (mOnBroadcasterEventListener != null) { + mOnBroadcasterEventListener.onStarted(); + } + } + + @Override + public void onStopped() { + if (mOnBroadcasterEventListener != null) { + mOnBroadcasterEventListener.onStopped(); + } + } + + @Override + public void onError(MediaEncoderException e) { + if (callback != null) { + callback.onFailed(e); + } + + if (mOnBroadcasterEventListener != null) { + mOnBroadcasterEventListener.onError(e); + } + AbstractSRTBroadcaster.this.stop(); + } + + @Override + public void onConnected() { + } + + @Override + public void onDisconnected() { + } + + @Override + public void onNewBitrate(long bitrate) { + } + }); + mSrtClient.start(); + } + + @Override + public void stop() { + if (mSrtClient != null) { + mSrtClient.stop(); + mSrtClient = null; + } + } + + @Override + public void setMute(boolean mute) { + if (mSrtClient != null) { + mSrtClient.setMute(mute); + } + } + + @Override + public boolean isMute() { + return mSrtClient != null && mSrtClient.isMute(); + } + + @Override + public void onConfigChange() { + super.onConfigChange(); + + if (mSrtClient != null) { + mSrtClient.restartVideoEncoder(); + } + } + + @Override + protected VideoQuality getVideoQuality() { + if (mSrtClient != null) { + VideoEncoder videoEncoder = mSrtClient.getVideoEncoder(); + if (videoEncoder != null) { + return videoEncoder.getVideoQuality(); + } + } + return null; + } + + @Override + protected AudioQuality getAudioQuality() { + if (mSrtClient != null) { + AudioEncoder audioEncoder = mSrtClient.getAudioEncoder(); + if (audioEncoder != null) { + return audioEncoder.getAudioQuality(); + } + } + return null; + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractSRTPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractSRTPreviewServer.java new file mode 100644 index 0000000000..9c254a5a9e --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/AbstractSRTPreviewServer.java @@ -0,0 +1,215 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import android.content.Context; +import android.util.Log; + +import org.deviceconnect.android.deviceplugin.uvc.BuildConfig; +import org.deviceconnect.android.deviceplugin.uvc.util.SRTSettings; +import org.deviceconnect.android.libmedia.streaming.audio.AudioEncoder; +import org.deviceconnect.android.libmedia.streaming.audio.AudioQuality; +import org.deviceconnect.android.libmedia.streaming.audio.MicAACLATMEncoder; +import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; +import org.deviceconnect.android.libmedia.streaming.video.VideoQuality; +import org.deviceconnect.android.libsrt.server.SRTServer; +import org.deviceconnect.android.libsrt.server.SRTSession; + +import java.io.IOException; + +public abstract class AbstractSRTPreviewServer extends AbstractPreviewServer { + private static final boolean DEBUG = BuildConfig.DEBUG; + private static final String TAG = "CameraSRT"; + + /** + * プレビュー配信サーバのマイムタイプを定義. + */ + private static final String MIME_TYPE = "video/MP2T"; + + /** + * SRTの設定. + */ + private final SRTSettings mSettings; + + /** + * SRT サーバ. + */ + private SRTServer mSRTServer; + + public AbstractSRTPreviewServer(Context context, MediaRecorder recorder) { + this(context, recorder, false); + } + + public AbstractSRTPreviewServer(Context context, MediaRecorder recorder, boolean useSSL) { + super(context, recorder, useSSL); + mSettings = new SRTSettings(context); + } + + + @Override + public String getUri() { + return "srt://localhost:" + getPort(); + } + + @Override + public String getMimeType() { + return MIME_TYPE; + } + + @Override + public void startWebServer(final OnWebServerStartCallback callback) { + if (mSRTServer == null) { + try { + mSRTServer = new SRTServer(getPort()); +// mSRTServer.setStatsInterval(BuildConfig.STATS_INTERVAL); + mSRTServer.setShowStats(DEBUG); + mSRTServer.setCallback(mCallback); + mSRTServer.setSocketOptions(mSettings.loadSRTSocketOptions()); + mSRTServer.start(); + } catch (Exception e) { + callback.onFail(); + return; + } + } + callback.onStart(getUri()); + } + + @Override + public void stopWebServer() { + if (mSRTServer != null) { + mSRTServer.stop(); + mSRTServer = null; + } + } + + @Override + public boolean requestSyncFrame() { + SRTServer server = mSRTServer; + if (server != null) { + SRTSession session = server.getSRTSession(); + if (session != null) { + VideoEncoder videoEncoder = session.getVideoEncoder(); + if (videoEncoder != null) { + videoEncoder.requestSyncKeyFrame(); + return true; + } + } + } + return false; + } + + @Override + public long getBPS() { + // TODO + return 0; + } + + @Override + public void onConfigChange() { + super.onConfigChange(); + restartVideoEncoder(); + } + + @Override + public void setMute(boolean mute) { + super.setMute(mute); + + if (mSRTServer != null) { + SRTSession session = mSRTServer.getSRTSession(); + if (session != null) { + AudioEncoder audioEncoder = session.getAudioEncoder(); + if (audioEncoder != null) { + audioEncoder.setMute(mute); + } + } + } + } + + @Override + protected VideoQuality getVideoQuality() { + if (mSRTServer != null) { + SRTSession session = mSRTServer.getSRTSession(); + if (session != null) { + VideoEncoder videoEncoder = session.getVideoEncoder(); + if (videoEncoder != null) { + return videoEncoder.getVideoQuality(); + } + } + } + return null; + } + + @Override + protected AudioQuality getAudioQuality() { + if (mSRTServer != null) { + SRTSession session = mSRTServer.getSRTSession(); + if (session != null) { + AudioEncoder audioEncoder = session.getAudioEncoder(); + if (audioEncoder != null) { + return audioEncoder.getAudioQuality(); + } + } + } + return null; + } + + /** + * エンコーダの設定を反映して、再スタートします. + */ + private void restartVideoEncoder() { + if (mSRTServer != null) { + SRTSession session = mSRTServer.getSRTSession(); + if (session != null) { + session.restartVideoEncoder(); + } + } + } + + /** + * SRT 用の映像エンコーダを作成します. + * + * @return SRT 用の映像エンコーダ + */ + protected VideoEncoder createVideoEncoder() { + return null; + } + + protected AudioEncoder createAudioEncoder() { + MediaRecorder recorder = getRecorder(); + MediaRecorder.Settings settings = recorder.getSettings(); + if (settings.isAudioEnabled()) { + return new MicAACLATMEncoder(); + } + return null; + } + + /** + * SRTServer からのイベントを受け取るためのコールバック. + */ + private final SRTServer.Callback mCallback = new SRTServer.Callback() { + @Override + public void createSession(final SRTSession session) { + if (DEBUG) { + Log.d(TAG, "SRTServer.Callback#createSession()"); + } + + VideoEncoder videoEncoder = createVideoEncoder(); + if (videoEncoder != null) { + setVideoQuality(videoEncoder.getVideoQuality()); + session.setVideoEncoder(videoEncoder); + } + + AudioEncoder audioEncoder = createAudioEncoder(); + if (audioEncoder != null) { + audioEncoder.setMute(isMuted()); + setAudioQuality(audioEncoder.getAudioQuality()); + session.setAudioEncoder(audioEncoder); + } + } + + @Override + public void releaseSession(final SRTSession session) { + if (DEBUG) { + Log.d(TAG, "SRTServer.Callback#releaseSession()"); + } + } + }; +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/Broadcaster.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/Broadcaster.java new file mode 100644 index 0000000000..8307f1461f --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/Broadcaster.java @@ -0,0 +1,100 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +public interface Broadcaster { + /** + * マイムタイプを取得します. + * + * @return マイムタイプ + */ + String getMimeType(); + + /** + * ブロードキャスト先の URI を取得します. + * + * @return ブロードキャスト先の URI + */ + String getBroadcastURI(); + + /** + * ブロードキャストのイベントを通知するリスナーを設定します. + * + * @param listener リスナー + */ + void setOnEventListener(OnEventListener listener); + + /** + * ブロードキャスト中か確認します. + * + * @return ブロードキャスト中の場合は true、それ以外は false + */ + boolean isRunning(); + + /** + * ブロードキャストを開始します. + */ + void start(OnStartCallback callback); + + /** + * ブロードキャストを停止します. + */ + void stop(); + + /** + * ミュート設定を行います. + * + * @param mute ミュートにする場合にはtrue、それ以外はfalse + */ + void setMute(boolean mute); + + /** + * ミュート設定を取得します. + * + * @return ミュートの場合はtrue、それ以外はfalse + */ + boolean isMute(); + + /** + * 設定が変更されたことを通知します. + */ + void onConfigChange(); + + /** + * ブロードキャストの開始結果を通知するコールバック. + */ + interface OnStartCallback { + /** + * ブロードキャストに成功したことを通知します. + */ + void onSuccess(); + + /** + * ブロードキャストに失敗したことを通知します. + * + * @param e 失敗原因の例外 + */ + void onFailed(Exception e); + } + + /** + * ブロードキャストのイベントを通知するリスナー. + */ + interface OnEventListener { + + /** + * ブロードキャストが開始されたことを通知します. + */ + void onStarted(); + + /** + * ブロードキャストが停止されたことを通知します. + */ + void onStopped(); + + /** + * ブロードキャストでエラーが発生したことを通知します. + * + * @param e エラー原因の例外 + */ + void onError(Exception e); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/BroadcasterProvider.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/BroadcasterProvider.java new file mode 100644 index 0000000000..e97d67de68 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/BroadcasterProvider.java @@ -0,0 +1,73 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +public interface BroadcasterProvider { + /** + * ブロードキャスターのリストを取得します. + * + * @return ブロードキャスターのリスト + */ + Broadcaster getBroadcaster(); + + /** + * ブロードキャスト中か確認します. + * + * @return ブロードキャスト中は true、それ以外は false + */ + boolean isRunning(); + + /** + * ブロードキャスターを開始します. + * + * @param broadcastURI 配信先の URI + */ + Broadcaster startBroadcaster(String broadcastURI); + + /** + * ブロードキャスターを停止します. + */ + void stopBroadcaster(); + + /** + * 設定が変更されたことを通知します. + */ + void onConfigChange(); + + /** + * Recorder をミュート状態にする. + */ + void setMute(boolean mute); + + /** + * BroadcasterProvider で発生したイベントを通知するリスナーを設定します. + * + * @param listener リスナー + */ + void setOnEventListener(OnEventListener listener); + + /** + * イベントを通知するリスナー. + */ + interface OnEventListener { + /** + * Broadcaster が開始されたことを通知します. + * + * @param broadcaster 開始した Broadcaster + */ + void onStarted(Broadcaster broadcaster); + + /** + * Broadcaster が停止されたことを通知します. + * + * @param broadcaster 停止した Broadcaster + */ + void onStopped(Broadcaster broadcaster); + + /** + * Broadcaster でエラーが発生したことを通知します. + * + * @param broadcaster エラーが発生した Broadcaster + * @param e エラー原因の例外 + */ + void onError(Broadcaster broadcaster, Exception e); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/MediaRecorder.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/MediaRecorder.java index bbaea1518c..9104126835 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/MediaRecorder.java +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/MediaRecorder.java @@ -6,311 +6,1145 @@ */ package org.deviceconnect.android.deviceplugin.uvc.recorder; -import android.os.Parcel; -import android.os.Parcelable; +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.util.Size; -import org.deviceconnect.android.deviceplugin.uvc.recorder.preview.PreviewServer; +import org.deviceconnect.android.deviceplugin.uvc.util.CapabilityUtil; +import org.deviceconnect.android.deviceplugin.uvc.util.PropertyUtil; +import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; +import java.util.ArrayList; import java.util.List; +import javax.net.ssl.SSLContext; + /** * メディアレコーダのインターフェース. * * @author NTT DOCOMO, INC. */ public interface MediaRecorder { + /** + * MediaRecorder で使用するデフォルトのマイムタイプ. + */ + String MIME_TYPE_JPEG = "image/jpeg"; /** - * レコーダの初期化処理を行います. + * MediaRecorder の初期化処理を行います. */ void initialize(); /** - * レコーダの後始末処理を行います. + * プロセス起動時の状態に戻す. + * + * プラグイン再起動時に呼び出すこと. */ void clean(); /** - * レコーダのIDを取得します. + * オブジェクトを破棄する. + * + * プロセス終了時に呼び出すこと. + */ + void destroy(); + + /** + * MediaRecorder を識別する ID を取得します. * - * @return レコーダID + * @return MediaRecorder を識別する ID */ String getId(); /** - * レコーダ名を取得します. + * MediaRecorder の名前を取得します. * - * @return レコーダ名 + * @return MediaRecorder の名前 */ String getName(); /** - * レコーダに設定されているマイムタイプを取得します. + * MediaRecorder のマイムタイプを取得します. * * @return マイムタイプ */ String getMimeType(); /** - * レコーダにマイムタイプを設定します. + * MediaRecorder の状態を取得します. * - * @param mimeType マイムタイプ + * @return MediaRecorder の状態 */ - void setMimeType(final String mimeType); + State getState(); /** - * レコーダの状態を取得します. + * HostMediaRecorder の設定を取得します. * - * @return レコーダの状態 + * @return HostMediaRecorder の設定 */ - State getState(); + Settings getSettings(); /** - * レコーダに設定されている写真サイズを取得します. + * プレビュー配信サーバの管理クラスを取得します. * - * @return 写真サイズ + * @return プレビュー配信サーバ */ - Size getPictureSize(); + PreviewServerProvider getServerProvider(); /** - * レコーダに写真サイズを設定します. + * プレビュー配信管理クラスを取得します. * - * @param size 写真サイズ + * @return BroadcasterProvider の実装クラス */ - void setPictureSize(Size size); + BroadcasterProvider getBroadcasterProvider(); /** - * レコーダに設定されているプレビューサイズを取得します. + * プレビュー配信サーバの動作状況を確認します. * - * @return プレビューサイズ + * @return プレビュー配信サーバが動作している場合はtrue、それ以外はfalse */ - Size getPreviewSize(); + boolean isPreviewRunning(); /** - * レコーダにプレビューサイズを設定します. + * プレビュー配信サーバを開始します. + * + * 開始できなかった場合には、空のリストを返します。 * - * @param size プレビューサイズ + * @return 開始したプレビュー配信サーバのリスト */ - void setPreviewSize(Size size); + List startPreview(); /** - * レコーダに設定されているフレームレートを取得します. - * - * @return フレームレート + * プレビュー配信サーバを停止します. */ - double getMaxFrameRate(); + void stopPreview(); /** - * レコーダにフレームレートを設定します. + * ブロードキャストの動作状況を確認します. * - * @param frameRate フレームレート + * @return ブロードキャストされている場合はtrue、それ以外はfalse */ - void setMaxFrameRate(double frameRate); + boolean isBroadcasterRunning(); /** - * レコーダに設定されているビットレートを取得します. + * ブロードキャストを開始します. * - * @return ビットレート + * @param broadcastURI ブロードキャスト先のURI + * @return ブロードキャストしているクラス */ - int getPreviewBitRate(); + Broadcaster startBroadcaster(String broadcastURI); /** - * レコーダにビットレートを設定します. - * - * @param bitRate ビットレート + * ブロードキャストを停止します. */ - void setPreviewBitRate(int bitRate); + void stopBroadcaster(); /** - * サポートしている写真サイズを取得します. + * 描画用オブジェクトを取得します. * - * @return サポートしている写真サイズ + * @return EGLSurfaceDrawingThread のインスタンス */ - List getSupportedPictureSizes(); + EGLSurfaceDrawingThread getSurfaceDrawingThread(); /** - * サポートしているプレビューサイズを取得します. + * ミュート設定を行います. * - * @return サポートしているプレビューサイズ + * @param mute ミュート設定 */ - List getSupportedPreviewSizes(); + void setMute(boolean mute); /** - * サポートしているマイムタイプを取得します. + * ミュート設定を取得します. * - * @return サポートしているマイムタイプ + * @return ミュートの場合はtrue、それ以外はfalse */ - List getSupportedMimeTypes(); + boolean isMute(); /** - * 写真撮影を行います. - * - * @param listener 写真撮影のイベントを通知するリスナー + * 設定が変更されたことを通知します. */ - void takePhoto(OnPhotoEventListener listener); + void onConfigChange(); /** - * プレビュー用のサーバを取得します. + * SSL コンテキストの設定を行います. * - * @return プレビュー用サーバ + * @param sslContext SSL コンテキスト */ - List getServers(); + void setSSLContext(SSLContext sslContext); /** - * プレビューを開始します. - * サーバが起動できなかった場合には、空のリストを返却する。 - * @return 起動したプレビュー配信サーバのリスト + * パーミッションの要求結果を通知するコールバックを設定します. + * + * @param callback コールバック */ - List startPreview(); + void requestPermission(PermissionCallback callback); /** - * プレビューを停止します. + * イベントを通知するためのリスナーを設定します. + * + * @param listener リスナー */ - void stopPreview(); + void setOnEventListener(OnEventListener listener); /** - * プレビューが廃止されているか. - * @return + * レコーダで映像を配信したデータの BPS を取得します. + * + * @return レコーダで映像を配信したデータの BPS */ - boolean isStartedPreview(); + long getBPS(); /** - * {@link #takePhoto(OnPhotoEventListener)} のイベントを受け取るリスナー. + * パーミッション結果通知用コールバック. */ - interface OnPhotoEventListener { + interface PermissionCallback { /** - * 写真が取られた時のイベントを通知します. - * - * @param uri 写真へのURI - * @param filePath 写真へのパス + * 許可された場合に呼び出されます. */ - void onTakePhoto(String uri, String filePath); + void onAllowed(); /** - * 写真撮影に失敗した時のイベントを通知します. - * - * @param errorMessage エラーメッセージ + * 拒否された場合に呼び出されます. */ - void onFailedTakePhoto(String errorMessage); + void onDisallowed(); } /** - * レコーダの状態. + * MediaRecorder の状態. */ enum State { /** - * アイドル中. + * 動作していない. */ - INACTTIVE, + INACTIVE, /** - * 一時停止中. + * 録画が一時停止中の状態. */ PAUSED, /** - * レコーディング中. + * 録画・静止画撮影中の状態. */ RECORDING, /** - * エラー中. + * プレビューが表示されている状態. + */ + PREVIEW, + + /** + * エラーで停止している状態. */ ERROR } /** - * レコーダで使用するサイズ. + * HostMediaRecorder のイベントを通知するリスナー. + */ + interface OnEventListener { + /** + * レコーダの設定が変更されたことを通知します. + */ + void onConfigChanged(); + + /** + * プレビュー配信を開始した時に呼び出されます. + * + * @param servers 開始したプレビュー配信サーバ + */ + void onPreviewStarted(List servers); + + /** + * プレビュー配信を停止した時に呼び出されます. + */ + void onPreviewStopped(); + + /** + * プレビュー配信でエラーが発生したときに呼び出されます. + * + * @param e エラー原因の例外 + */ + void onPreviewError(Exception e); + + /** + * ブロードキャストを開始した時に呼び出されます. + * + * @param broadcaster 開始したブロードキャスト + */ + void onBroadcasterStarted(Broadcaster broadcaster); + + /** + * ブロードキャストを停止した時に呼び出されます. + * + * @param broadcaster 停止したブロードキャスト + */ + void onBroadcasterStopped(Broadcaster broadcaster); + + /** + * ブロードキャストでエラーが発生したときに呼び出されます. + * + * @param broadcaster エラーが発生した Broadcaster + * @param e エラー原因の例外 + */ + void onBroadcasterError(Broadcaster broadcaster, Exception e); + + /** + * レコーダで発生したエラーを通知します. + * + * @param e エラー原因の例外 + */ + void onError(Exception e); + } + + enum AudioSource { + DEFAULT("default"), + MIC("mic"), + APP("app"); + + private final String mSource; + + AudioSource(String source) { + mSource = source; + } + + public String getValue() { + return mSource; + } + + public static AudioSource typeOf(String source) { + for (AudioSource audioSource : values()) { + if (audioSource.mSource.equalsIgnoreCase(source)) { + return audioSource; + } + } + return null; + } + } + + enum VideoEncoderName { + H264("h264", "video/avc"), + H265("h265", "video/hevc"); + + private final String mName; + private final String mMimeType; + + VideoEncoderName(String name, String mimeType) { + mName = name; + mMimeType = mimeType; + } + + public String getName() { + return mName; + } + + public String getMimeType() { + return mMimeType; + } + + public static VideoEncoderName nameOf(String name) { + for (VideoEncoderName encoder : values()) { + if (encoder.getName().equalsIgnoreCase(name)) { + return encoder; + } + } + return H264; + } + } + + class ProfileLevel { + private final int mProfile; + private final int mLevel; + + public ProfileLevel(int profile, int level) { + mProfile = profile; + mLevel = level; + } + + public int getProfile() { + return mProfile; + } + + public int getLevel() { + return mLevel; + } + } + + /** + * HostMediaRecorder の設定を保持するクラス. */ - class Size implements Parcelable { + abstract class Settings { + private final PropertyUtil mPref; + + public Settings(Context context, String name) { + mPref = new PropertyUtil(context, name); + } + /** - * 横幅. + * 初期化されているか確認します. + * + * @return 初期化されている場合はtrue、それ以外はfalse */ - private final int mWidth; + public boolean isInitialized() { + return mPref.getString("test", null) != null; + } /** - * 縦幅. + * 初期化完了を書き込みます. */ - private final int mHeight; + public void finishInitialization() { + mPref.put("test", "test"); + } /** - * コンストラクタ. - * @param w 横幅 - * @param h 縦幅 + * 保存データを初期化します. */ - public Size(final int w, final int h) { - mWidth = w; - mHeight = h; + public void clear() { + mPref.clear(); } /** - * コンストラクタ. - * @param in Parcelのストリーム + * 写真サイズを取得します. + * + * @return 写真サイズ */ - private Size(final Parcel in) { - this(in.readInt(), in.readInt()); + public Size getPictureSize() { + return mPref.getSize("picture_size_width", "picture_size_height"); } /** - * 横幅を取得します. + * 写真サイズを設定します. + * + * サポートされていない写真サイズの場合は IllegalArgumentException を発生させます。 * - * @return 横幅 + * @param pictureSize 写真サイズ */ - public int getWidth() { - return mWidth; + public void setPictureSize(Size pictureSize) { + if (!isSupportedPictureSize(pictureSize)) { + throw new IllegalArgumentException("pictureSize is not supported."); + } + mPref.put( + "picture_size_width", + "picture_size_height", + pictureSize); } /** - * 縦幅を取得します. + * プレビューサイズを取得します. * - * @return 縦幅 + * @return プレビューサイズ */ - public int getHeight() { - return mHeight; + public Size getPreviewSize() { + return mPref.getSize("preview_size_width", "preview_size_height"); } - @Override - public int describeContents() { - return 0; + /** + * プレビューサイズを設定します. + * + * サポートされていないプレビューサイズの場合は IllegalArgumentException を発生させます。 + * + * @param previewSize プレビューサイズ + */ + public void setPreviewSize(Size previewSize) { + if (!isSupportedPreviewSize(previewSize)) { + throw new IllegalArgumentException("previewSize is not supported."); + } + mPref.put("preview_size_width", "preview_size_height", previewSize); } - @Override - public void writeToParcel(final Parcel out, final int flags) { - out.writeInt(mWidth); - out.writeInt(mHeight); + /** + * フレームレートを取得します. + * + * @return フレームレート + */ + public int getPreviewMaxFrameRate() { + return mPref.getInteger("preview_framerate", 30); } - public static final Creator CREATOR = new Creator() { - @Override - public Size createFromParcel(Parcel in) { - return new Size(in); + /** + * フレームレートを設定します. + * + * @param previewMaxFrameRate フレームレート + */ + public void setPreviewMaxFrameRate(Integer previewMaxFrameRate) { + if (previewMaxFrameRate <= 0) { + throw new IllegalArgumentException("previewMaxFrameRate is zero or negative."); } - @Override - public Size[] newArray(int size) { - return new Size[size]; + mPref.put("preview_framerate", previewMaxFrameRate); + } + + /** + * ビットレートを取得します. + * + * @return ビットレート(byte) + */ + public int getPreviewBitRate() { + return mPref.getInteger("preview_bitrate", 2 * 1024 * 1024); + } + + /** + * ビットレートを設定します. + * + * @param previewBitRate ビットレート(byte) + */ + public void setPreviewBitRate(int previewBitRate) { + if (previewBitRate <= 0) { + throw new IllegalArgumentException("previewBitRate is zero or negative."); } - }; + mPref.put("preview_bitrate", String.valueOf(previewBitRate)); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + /** + * キーフレームインターバルを取得します. + * + * @return キーフレームを発行する間隔(ミリ秒) + */ + public int getPreviewKeyFrameInterval() { + return mPref.getInteger("preview_i_frame_interval", 1); + } - Size size = (Size) o; + /** + * キーフレームインターバルを設定します. + * + * @param previewKeyFrameInterval キーフレームを発行する間隔(ミリ秒) + */ + public void setPreviewKeyFrameInterval(int previewKeyFrameInterval) { + if (previewKeyFrameInterval <= 0) { + throw new IllegalArgumentException("previewKeyFrameInterval is zero or negative."); + } + mPref.put("preview_i_frame_interval", previewKeyFrameInterval); + } - return mWidth == size.mWidth && mHeight == size.mHeight; + /** + * プレビュー配信エンコード名を取得します. + * + * @return エンコード名 + */ + public VideoEncoderName getPreviewEncoderName() { + return VideoEncoderName.nameOf(getPreviewEncoder()); } - @Override - public int hashCode() { - int result = mWidth; - result = 31 * result + mHeight; - return result; + /** + * プレビューの配信エンコードの名前を取得します. + * + * 未設定の場合は h264 を返却します。 + * + * @return プレビューの配信エンコードの名前 + */ + public String getPreviewEncoder() { + return mPref.getString("preview_encoder", "h264"); + } + + /** + * プレビューの配信エンコードの名前を設定します. + * + * @param encoder プレビューの配信エンコードの名前 + */ + public void setPreviewEncoder(String encoder) { + if (encoder == null) { + mPref.remove("preview_encoder"); + } else { + if (!isSupportedVideoEncoder(encoder)) { + throw new IllegalArgumentException("encoder is not supported."); + } + mPref.put("preview_encoder", encoder); + } + } + + /** + * ソフトウェアエンコーダを優先的に使用するフラグを確認します. + * + * @return ソフトウェアエンコーダを優先的に使用する場合は true、それ以外は false + */ + public boolean isUseSoftwareEncoder() { + return mPref.getBoolean("preview_use_software_encoder", false); } - @Override - public String toString() { - return "(width = " + getWidth() + ", height = " + getHeight() + ")"; + /** + * ソフトウェアエンコーダを優先的に使用するフラグを設定します. + * + * @param used ソフトウェアエンコーダを優先的に使用する場合は true、それ以外は false + */ + public void setUseSoftwareEncoder(boolean used) { + mPref.put("preview_use_software_encoder", used); + } + + /** + * イントラリフレッシュのフレーム数を取得します. + * + * @return イントラリフレッシュのフレーム数 + */ + public Integer getIntraRefresh() { + return mPref.getInteger("preview_intra_refresh", 0); + } + + /** + * イントラリフレッシュのフレーム数を設定します. + * + * @param refresh イントラリフレッシュのフレーム数 + */ + public void setIntraRefresh(Integer refresh) { + if (refresh == null) { + mPref.remove("preview_intra_refresh"); + } else { + mPref.put("preview_intra_refresh", refresh); + } + } + + /** + * プロファイルとレベルを取得します. + * + * 未設定の場合には、null を返却します。 + * + * @return プロファイルとレベル + */ + public ProfileLevel getProfileLevel() { + Integer profile = mPref.getInteger("preview_profile", null); + Integer level = mPref.getInteger("preview_level", null); + if (profile != null && level != null) { + return new ProfileLevel(profile, level); + } + return null; + } + + /** + * プロファイルとレベルを設定します. + * + * null が設定された場合には、未設定にします。 + * + * サポートされていないプロファイルとレベルが設定された場合には例外を発生します。 + * + * @param pl プロファイルとレベル + */ + public void setProfileLevel(ProfileLevel pl) { + if (pl == null) { + mPref.remove("preview_profile"); + mPref.remove("preview_level"); + } else { + if (!isSupportedProfileLevel(pl.getProfile(), pl.getLevel())) { + throw new IllegalArgumentException("profile and level are not supported."); + } + mPref.put("preview_profile", pl.getProfile()); + mPref.put("preview_level", pl.getLevel()); + } + } + + /** + * 設定されているプロファイルを取得します. + * + * @return プロファイル + */ + public Integer getProfile() { + return mPref.getInteger("preview_profile", 0); + } + + /** + * 設定されているレベルを取得します. + * + * @return レベル + */ + public Integer getLevel() { + return mPref.getInteger("preview_level", 0); + } + + /** + * プレビューの品質を取得します. + * + * @return プレビューの品質 + */ + public int getPreviewQuality() { + return mPref.getInteger("preview_jpeg_quality", 80); + } + + /** + * プレビューの品質を設定します. + * + * 0 から 100 の間で設定することができます。 + * それ以外は例外が発生します。 + * + * @param quality プレビューの品質 + */ + public void setPreviewQuality(int quality) { + if (quality < 0) { + throw new IllegalArgumentException("quality is negative value."); + } + if (quality > 100) { + throw new IllegalArgumentException("quality is over 100."); + } + mPref.put("preview_jpeg_quality", quality); + } + + /** + * 切り抜き範囲を取得します. + * + * 範囲ば設定されていない場合には、null を返却します. + * + * @return 切り抜き範囲 + */ + public Rect getDrawingRange() { + return mPref.getRect("preview_clip_left", + "preview_clip_top", + "preview_clip_right", + "preview_clip_bottom"); + } + + /** + * 切り抜き範囲を設定します. + * + * 引数に null が指定された場合には、切り抜き範囲を削除します。 + * + * @param rect 切り抜き範囲 + */ + public void setDrawingRange(Rect rect) { + if (rect == null) { + mPref.remove("preview_clip_left"); + mPref.remove("preview_clip_top"); + mPref.remove("preview_clip_right"); + mPref.remove("preview_clip_bottom"); + } else { + mPref.put( + "preview_clip_left", + "preview_clip_top", + "preview_clip_right", + "preview_clip_bottom", + rect); + } + } + + /** + * サポートしている写真サイズを取得します. + * + * サポートしていない場合には空のリストを返却します。 + * + * @return サポートしている写真サイズ + */ + public List getSupportedPictureSizes() { + return new ArrayList<>(); + } + + /** + * サポートしているプレビューサイズを取得します. + * + * サポートしていない場合には空のリストを返却します。 + * + * @return サポートしているプレビューサイズ + */ + public List getSupportedPreviewSizes() { + return new ArrayList<>(); + } + + /** + * サポートしているエンコーダのリストを取得します. + * + * @return サポートしているエンコーダのリスト + */ + public List getSupportedVideoEncoders() { + List list = new ArrayList<>(); + List supported = CapabilityUtil.getSupportedVideoEncoders(); + for (VideoEncoderName encoderName : VideoEncoderName.values()) { + if (supported.contains(encoderName.getMimeType())) { + list.add(encoderName.getName()); + } + } + return list; + } + + /** + * サポートしているプロファイル・レベルのリストを取得します. + * + * @return サポートしているプロファイル・レベルのリスト + */ + public List getSupportedProfileLevel() { + VideoEncoderName encoderName = getPreviewEncoderName(); + return CapabilityUtil.getSupportedProfileLevel(encoderName.getMimeType()); + } + + /** + * 指定されたサイズがサポートされているか確認します. + * + * @param size 確認するサイズ + * @return サポートされている場合はtrue、それ以外はfalse + */ + public boolean isSupportedPictureSize(final Size size) { + for (Size s : getSupportedPictureSizes()) { + if (s.getWidth() == size.getWidth() + && s.getHeight() == size.getHeight()) { + return true; + } + } + return false; + } + + /** + * 指定されたサイズが静止画でサポートされているか確認します. + * + * @param width 横幅 + * @param height 縦幅 + * @return サポートされている場合はtrue、それ以外はfalse + */ + public boolean isSupportedPictureSize(int width, int height) { + return isSupportedPictureSize(new Size(width, height)); + } + + /** + * 指定されたサイズがプレビューでサポートされているか確認します. + * + * @param size 確認するサイズ + * @return サポートされている場合はtrue、それ以外はfalse + */ + public boolean isSupportedPreviewSize(Size size) { + for (Size s : getSupportedPreviewSizes()) { + if (s.getWidth() == size.getWidth() + && s.getHeight() == size.getHeight()) { + return true; + } + } + return false; + } + + /** + * 指定されたサイズがプレビューでサポートされているか確認します. + * + * @param width 横幅 + * @param height 縦幅 + * @return サポートされている場合はtrue、それ以外はfalse + */ + public boolean isSupportedPreviewSize(int width, int height) { + return isSupportedPreviewSize(new Size(width, height)); + } + + /** + * 指定されたエンコーダがサポートされているか確認します. + * + * @param encoder エンコーダ名 + * @return サポートされている場合はtrue、それ以外はfalse + */ + public boolean isSupportedVideoEncoder(String encoder) { + List encoderList = getSupportedVideoEncoders(); + if (encoderList != null) { + for (String e : encoderList) { + if (e.equalsIgnoreCase(encoder)) { + return true; + } + } + } + return false; + } + + /** + * 指定されたプロファイルとレベルがサポートされているか確認します. + * + * @param profile プロファイル + * @param level レベル + * @return サポートされている場合はtrue、それ以外はfalse + */ + public boolean isSupportedProfileLevel(int profile, int level) { + List list = getSupportedProfileLevel(); + if (list != null) { + for (ProfileLevel pl : list) { + if (profile == pl.getProfile() && level == pl.getLevel()) { + return true; + } + } + } + return false; + } + + // 音声 + + /** + * プレビュー音声が有効化確認します. + * + * @return プレビュー音声が有効の場合はtrue、それ以外はfalse + */ + public boolean isAudioEnabled() { + return getPreviewAudioSource() != null; + } + + /** + * プレビューの音声タイプを取得します. + * + * @return 音声タイプ + */ + public AudioSource getPreviewAudioSource() { + return AudioSource.typeOf(mPref.getString("preview_audio_source", "none")); + } + + /** + * プレビューの音声タイプを設定します. + * + * @param audioSource 音声タイプ + */ + public void setPreviewAudioSource(AudioSource audioSource) { + if (audioSource == null) { + mPref.put("preview_audio_source", "none"); + } else { + mPref.put("preview_audio_source", audioSource.mSource); + } + } + + /** + * プレビュー音声のビットレートを取得します. + * + * @return プレビュー音声のビットレート + */ + public int getPreviewAudioBitRate() { + return mPref.getInteger("preview_audio_bitrate", 64 * 1024); + } + + /** + * プレビュー音声のビットレートを設定します. + * + * @param bitRate プレビュー音声のビットレート + */ + public void setPreviewAudioBitRate(int bitRate) { + if (bitRate <= 0) { + throw new IllegalArgumentException("previewAudioBitRate is zero or negative value."); + } + mPref.put("preview_audio_bitrate", bitRate); + } + + /** + * プレビュー音声のサンプルレートを取得します. + * + * @return プレビュー音声のサンプルレート + */ + public int getPreviewSampleRate() { + return mPref.getInteger("preview_audio_sample_rate", 8000); + } + + /** + * プレビュー音声のサンプルレートを設定します. + * + * @param sampleRate プレビュー音声のサンプルレート + */ + public void setPreviewSampleRate(int sampleRate) { + mPref.put("preview_audio_sample_rate", sampleRate); + } + + /** + * プレビュー音声のチャンネル数を取得します. + * + * @return プレビュー音声のチャンネル数 + */ + public int getPreviewChannel() { + return mPref.getInteger("preview_audio_channel", 1); + } + + /** + * プレビュー音声のチャンネル数を設定します. + * + * @param channel プレビュー音声のチャンネル数 + */ + public void setPreviewChannel(int channel) { + mPref.put("preview_audio_channel", channel); + } + + /** + * プレビュー配信のエコーキャンセラーを取得します. + * + * @return プレビュー配信のエコーキャンセラー + */ + public boolean isUseAEC() { + return mPref.getBoolean("preview_audio_aec", true); + } + + /** + * プレビュー配信のエコーキャンセラーを設定します. + * + * @param used プレビュー配信のエコーキャンセラー + */ + public void setUseAEC(boolean used) { + mPref.put("preview_audio_aec", used); + } + + /** + * プレビューの音声ミュート設定を確認します. + * + * @return ミュートの場合はtrue、それ以外の場合はfalse + */ + public boolean isMute() { + return mPref.getBoolean("preview_audio_mute", false); + } + + /** + * プレビューの音声ミュート設定を行います. + * + * @param mute ミュートにする場合はtrue、それ以外はfalse + */ + public void setMute(boolean mute) { + mPref.put("preview_audio_mute", mute); + } + + public boolean isSupportedAudioSource(AudioSource source) { + List sourceList = getSupportedAudioSource(); + if (sourceList != null) { + for (AudioSource s : sourceList) { + if (s == source) { + return true; + } + } + } + return false; + } + + /** + * サポートしている音声入力のリストを取得します. + * + * @return サポートしている音声入力のリスト + */ + public List getSupportedAudioSource() { + List list = new ArrayList<>(); + for (AudioSource audioSource : AudioSource.values()) { + if (audioSource == AudioSource.APP) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + list.add(audioSource); + } + } else { + list.add(audioSource); + } + } + return list; + } + + // 配信 + + /** + * 配信先の URI を取得します. + * + * 設定されていない場合は null を返却します. + * + * @return 配信先の URI + */ + public String getBroadcastURI() { + return mPref.getString("broadcast_uri", null); + } + + /** + * 配信先の URI を設定します. + * + * @param broadcastURI 配信先の URI + */ + public void setBroadcastURI(String broadcastURI) { + mPref.put("broadcast_uri", broadcastURI); + } + + /** + * リトライ回数を取得します. + * + * @return リトライ回数 + */ + public int getRetryCount() { + return mPref.getInteger("broadcast_retry_count", 0); + } + + /** + * リトライ回数を設定します. + * + * @param count リトライ回数 + */ + public void setRetryCount(int count) { + if (count < 0) { + mPref.remove("broadcast_retry_count"); + } else { + mPref.put("broadcast_retry_count", count); + } + } + + /** + * リトライのインターバルを取得します. + * + * @return リトライのインターバル + */ + public int getRetryInterval() { + return mPref.getInteger("broadcast_retry_interval", 3000); + } + + /** + * リトライのインターバルを設定します. + * + * @param interval リトライのインターバル + */ + public void setRetryInterval(int interval) { + if (interval < 0) { + mPref.remove("broadcast_retry_interval"); + } else { + mPref.put("broadcast_retry_interval", interval); + } + } + + // ポート番号 + + /** + * Motion JPEG サーバ用のポート番号を取得します. + * + * @return Motion JPEG サーバ用のポート番号 + */ + public Integer getMjpegPort() { + return mPref.getInteger("mjpeg_port", 0); + } + + /** + * Motion JPEG サーバ用のポート番号を設定します. + * + * @param port Motion JPEG サーバ用のポート番号 + */ + public void setMjpegPort(int port) { + mPref.put("mjpeg_port", port); + } + + /** + * SSL で暗号化された Motion JPEG サーバ用のポート番号を取得します. + * + * @return Motion JPEG サーバ用のポート番号 + */ + public Integer getMjpegSSLPort() { + return mPref.getInteger("mjpeg_ssl_port", 0); + } + + /** + * SSL で暗号化された Motion JPEG サーバ用のポート番号を取得します. + * + * @param port Motion JPEG サーバ用のポート番号 + */ + public void setMjpegSSLPort(int port) { + mPref.put("mjpeg_ssl_port", port); + } + + /** + * RTSP サーバ用のポート番号を取得します. + * + * @return RTSP サーバ用のポート番号 + */ + public Integer getRtspPort() { + return mPref.getInteger("rtsp_port", 0); + } + + /** + * RTSP サーバ用のポート番号を設定します. + * + * @param port RTSP サーバ用のポート番号 + */ + public void setRtspPort(int port) { + mPref.put("rtsp_port", port); + } + + /** + * SRT サーバ用のポート番号を取得します. + * + * @return SRT サーバ用のポート番号 + */ + public Integer getSrtPort() { + return mPref.getInteger("srt_port", 0); + } + + /** + * SRT サーバ用のポート番号を設定します. + * + * @param port SRT サーバ用のポート番号 + */ + public void setSrtPort(int port) { + mPref.put("srt_port", port); } } } diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/MediaRecorderManager.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/MediaRecorderManager.java new file mode 100644 index 0000000000..290bfa00b8 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/MediaRecorderManager.java @@ -0,0 +1,453 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.h264.UvcH264Recorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.mjpeg.UvcMjpgRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.uncompressed.UvcUncompressedRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.uvc.UvcRecorder; +import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; +import org.deviceconnect.android.libmedia.streaming.util.WeakReferenceList; +import org.deviceconnect.android.libuvc.Parameter; +import org.deviceconnect.android.libuvc.UVCCamera; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class MediaRecorderManager { + /** + * レコーディング停止アクションを定義. + */ + public static final String ACTION_STOP_RECORDING = "org.deviceconnect.android.deviceplugin.uvc.STOP_RECORDING"; + + /** + * プレビュー停止用アクションを定義. + */ + public static final String ACTION_STOP_PREVIEW = "org.deviceconnect.android.deviceplugin.uvc.STOP_PREVIEW"; + + /** + * ブロードキャスト停止用アクションを定義. + */ + public static final String ACTION_STOP_BROADCAST = "org.deviceconnect.android.deviceplugin.uvc.STOP_BROADCAST"; + + /** + * レコーダのIDを格納するためのキーを定義. + */ + public static final String KEY_RECORDER_ID = "recorder_id"; + + /** + * プレビュー停止・レコーディング停止・ブロードキャスト停止・画面の回転イベントなどを受け取るための BroadcastReceiver. + */ + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + String action = intent.getAction(); + if (ACTION_STOP_PREVIEW.equals(action)) { + stopPreviewServer(intent.getStringExtra(KEY_RECORDER_ID)); + } else if (ACTION_STOP_RECORDING.equals(action)) { + stopRecording(intent.getStringExtra(KEY_RECORDER_ID)); + } else if (ACTION_STOP_BROADCAST.equals(action)) { + stopBroadcast(intent.getStringExtra(KEY_RECORDER_ID)); + } + } + }; + + /** + * レコーダのリスト. + */ + private final List mUvcRecorderList = new ArrayList<>(); + private final Context mContext; + + /** + * 各レコーダのイベントを通知するためのリスナー. + */ + private final WeakReferenceList mOnEventListeners = new WeakReferenceList<>(); + + /** + * コンストラクタ. + * + * @param context コンテキスト + */ + public MediaRecorderManager(Context context, UVCCamera uvcCamera) { + mContext = context; + initRecorders(context, uvcCamera); + + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_STOP_PREVIEW); + filter.addAction(ACTION_STOP_RECORDING); + filter.addAction(ACTION_STOP_BROADCAST); + mContext.registerReceiver(mBroadcastReceiver, filter); + } + + /** + * 後始末を行います. + */ + public void destroy() { + try { + mContext.unregisterReceiver(mBroadcastReceiver); + } catch (Exception e) { + // ignore. + } + + mOnEventListeners.clear(); + + for (MediaRecorder recorder : mUvcRecorderList) { + try { + recorder.destroy(); + } catch (Exception e) { + // ignore. + } + } + + mUvcRecorderList.clear(); + } + + /** + * レコーダが使用できるか確認します. + * + * @param recorder レコーダ + * @return 使用できる場合はtrue、それ以外はfalse + */ + public boolean canUseRecorder(UvcRecorder recorder) { + for (UvcRecorder uvcRecorder : getUvcRecorderList()) { + if (uvcRecorder == recorder) { + continue; + } + if (uvcRecorder.isPreviewRunning() || uvcRecorder.isBroadcasterRunning() || + uvcRecorder.getState() == MediaRecorder.State.RECORDING) { + // カメラが使用中 + return false; + } + } + return true; + } + + /** + * 使用するレコーダ以外のレコーダを停止します。 + * + * プレビュー配信中などの停止できない場合には、停止しません。 + * その場合には、カメラを使用できないので注意が必要になります。 + * + * @param useRecorder 使用するレコーダ + */ + public void stopCameraRecorder(MediaRecorder useRecorder) { + for (MediaRecorder recorder : getUvcRecorderList()) { + if (recorder == useRecorder) { + continue; + } + + if (recorder instanceof UvcRecorder) { + if (!recorder.isPreviewRunning() && !recorder.isBroadcasterRunning() && + recorder.getState() != MediaRecorder.State.RECORDING) { + // 強制的にカメラを停止 + EGLSurfaceDrawingThread drawingThread = recorder.getSurfaceDrawingThread(); + drawingThread.stop(true); + + // カメラの処理は別スレッドで行われているので、ここで少し待機します + try { + Thread.sleep(200); + } catch (InterruptedException e) { + // ignore. + } + } + } + } + } + + + /** + * レコーダのリストを取得します. + * + * @return レコーダ + */ + public List getUvcRecorderList() { + return mUvcRecorderList; + } + + /** + * レコーダ ID を指定してレコーダを取得します. + * + * レコーダが見つからない場合は null を返却します。 + * + * @param id レコーダID + * @return レコーダ + */ + public UvcRecorder findUvcRecorderById(String id) { + if (id != null) { + for (UvcRecorder recorder : getUvcRecorderList()) { + if (id.equalsIgnoreCase(recorder.getId())) { + return recorder; + } + } + } + return null; + } + + /** + * デフォルトのレコーダを取得します. + * + * レコーダが存在しない場合には null を返却します。 + * + * @return デフォルトのレコーダ + */ + public UvcRecorder getDefaultRecorder() { + if (mUvcRecorderList.isEmpty()) { + return null; + } + return mUvcRecorderList.get(0); + } + + /** + * レコーダの初期化処理を行います. + * + * @param context コンテキスト + * @param camera UVC デバイス + */ + private void initRecorders(Context context, UVCCamera camera) { + boolean hasMJPEG = false; + boolean hasH264 = false; + boolean hasUncompressed = false; + try { + List parameters = camera.getParameter(); + for (Parameter p : parameters) { + switch (p.getFrameType()) { + case UNCOMPRESSED: + hasUncompressed = true; + break; + case MJPEG: + hasMJPEG = true; + if (p.hasExtH264()) { + // Extension Unit を持っている場合に H264 として使用できる。 + hasH264 = true; + } + break; + case H264: + hasH264 = true; + break; + } + } + } catch (IOException e) { + // ignore. + } + + if (hasMJPEG) { + addUvcRecorder(new UvcMjpgRecorder(context, camera)); + } + + if (hasH264) { + addUvcRecorder(new UvcH264Recorder(context, camera)); + } + + if (hasUncompressed) { + addUvcRecorder(new UvcUncompressedRecorder(context, camera)); + } + + for (MediaRecorder recorder : mUvcRecorderList) { + recorder.initialize(); + } + } + + /** + * UvcRecorder をリストに追加します. + * + * @param mediaRecorder 追加するレコーダ + */ + private void addUvcRecorder(UvcRecorder mediaRecorder) { + mediaRecorder.setOnEventListener(new MediaRecorder.OnEventListener() { + @Override + public void onConfigChanged() { + postOnConfigChanged(mediaRecorder); + } + + @Override + public void onPreviewStarted(List servers) { + postOnPreviewStarted(mediaRecorder, servers); + } + + @Override + public void onPreviewStopped() { + postOnPreviewStopped(mediaRecorder); + } + + @Override + public void onPreviewError(Exception e) { + postOnPreviewError(mediaRecorder, e); + } + + @Override + public void onBroadcasterStarted(Broadcaster broadcaster) { + postOnBroadcasterStarted(mediaRecorder, broadcaster); + } + + @Override + public void onBroadcasterStopped(Broadcaster broadcaster) { + postOnBroadcasterStopped(mediaRecorder, broadcaster); + } + + @Override + public void onBroadcasterError(Broadcaster broadcaster, Exception e) { + postOnBroadcasterError(mediaRecorder, broadcaster, e); + } + + @Override + public void onError(Exception e) { + postOnError(mediaRecorder, e); + } + }); + mUvcRecorderList.add(mediaRecorder); + } + + /** + * 指定された ID のレコーダのプレビューを停止します. + * + * @param id レコーダの ID + */ + private void stopPreviewServer(final String id) { + MediaRecorder recorder = findUvcRecorderById(id); + if (recorder != null) { + recorder.stopPreview(); + } + } + + /** + * 指定された ID のレコードのブロードキャストを停止します. + * + * @param id レコードの ID + */ + private void stopBroadcast(final String id) { + MediaRecorder recorder = findUvcRecorderById(id); + if (recorder != null) { + recorder.stopBroadcaster(); + } + } + + /** + * 指定された ID のレコードの録音・録画を停止します. + * + * @param id レコードの ID + */ + private void stopRecording(final String id) { + } + + /** + * イベント通知用のリスナーを追加します. + * + * @param listener 追加するリスナー + */ + public void addOnEventListener(OnEventListener listener) { + mOnEventListeners.add(listener); + } + + /** + * イベント通知用のリスナーを削除します. + * + * @param listener 削除するリスナー + */ + public void removeOnEventListener(OnEventListener listener) { + mOnEventListeners.remove(listener); + } + + private void postOnConfigChanged(MediaRecorder recorder) { + for (OnEventListener l : mOnEventListeners) { + l.onConfigChanged(recorder); + } + } + + private void postOnPreviewStarted(MediaRecorder recorder, List servers) { + for (OnEventListener l : mOnEventListeners) { + l.onPreviewStarted(recorder, servers); + } + } + + private void postOnPreviewStopped(MediaRecorder recorder) { + for (OnEventListener l : mOnEventListeners) { + l.onPreviewStopped(recorder); + } + } + + private void postOnPreviewError(MediaRecorder recorder, Exception e) { + for (OnEventListener l : mOnEventListeners) { + l.onPreviewError(recorder, e); + } + } + + private void postOnBroadcasterStarted(MediaRecorder recorder, Broadcaster broadcaster) { + for (OnEventListener l : mOnEventListeners) { + l.onBroadcasterStarted(recorder, broadcaster); + } + } + + private void postOnBroadcasterStopped(MediaRecorder recorder, Broadcaster broadcaster) { + for (OnEventListener l : mOnEventListeners) { + l.onBroadcasterStopped(recorder, broadcaster); + } + } + + private void postOnBroadcasterError(MediaRecorder recorder, Broadcaster broadcaster, Exception e) { + for (OnEventListener l : mOnEventListeners) { + l.onBroadcasterError(recorder, broadcaster, e); + } + } + +// private void postOnTakePhoto(MediaRecorder recorder, String uri, String filePath, String mimeType) { +// for (OnEventListener l : mOnEventListeners) { +// l.onTakePhoto(recorder, uri, filePath, mimeType); +// } +// } +// +// private void postOnRecordingStarted(MediaRecorder recorder, String fileName) { +// for (OnEventListener l : mOnEventListeners) { +// l.onRecordingStarted(recorder, fileName); +// } +// } +// +// private void postOnRecordingPause(MediaRecorder recorder) { +// for (OnEventListener l : mOnEventListeners) { +// l.onRecordingPause(recorder); +// } +// } +// +// private void postOnRecordingResume(MediaRecorder recorder) { +// for (OnEventListener l : mOnEventListeners) { +// l.onRecordingResume(recorder); +// } +// } +// +// private void postOnRecordingStopped(MediaRecorder recorder, String fileName) { +// for (OnEventListener l : mOnEventListeners) { +// l.onRecordingStopped(recorder, fileName); +// } +// } + + private void postOnError(MediaRecorder recorder, Exception e) { + for (OnEventListener l : mOnEventListeners) { + l.onError(recorder, e); + } + } + + public interface OnEventListener { + void onConfigChanged(MediaRecorder recorder); + + void onPreviewStarted(MediaRecorder recorder, List servers); + void onPreviewStopped(MediaRecorder recorder); + void onPreviewError(MediaRecorder recorder, Exception e); + + void onBroadcasterStarted(MediaRecorder recorder, Broadcaster broadcaster); + void onBroadcasterStopped(MediaRecorder recorder, Broadcaster broadcaster); + void onBroadcasterError(MediaRecorder recorder, Broadcaster broadcaster, Exception e); + +// void onTakePhoto(MediaRecorder recorder, String uri, String filePath, String mimeType); +// +// void onRecordingStarted(MediaRecorder recorder, String fileName); +// void onRecordingPause(MediaRecorder recorder); +// void onRecordingResume(MediaRecorder recorder); +// void onRecordingStopped(MediaRecorder recorder, String fileName); + + void onError(MediaRecorder recorder, Exception e); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/PreviewServer.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/PreviewServer.java new file mode 100644 index 0000000000..aba34d3d39 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/PreviewServer.java @@ -0,0 +1,122 @@ +/* + PreviewServer.java + Copyright (c) 2017 NTT DOCOMO,INC. + Released under the MIT license + http://opensource.org/licenses/mit-license.php + */ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import javax.net.ssl.SSLContext; + +/** + * プレビュー配信用サーバを定義するインターフェース. + */ +public interface PreviewServer { + /** + * サーバが配信するプレビューのマイムタイプを取得します. + * + * @return マイムタイプ + */ + String getMimeType(); + + /** + * サーバへの URL を取得します. + * + * @return サーバへの URL + */ + String getUri(); + + /** + * プレビュー配信サーバのポート番号を取得します. + * + * @return ポート番号 + */ + int getPort(); + + /** + * プレビュー配信サーバのポート番号を設定します. + * + * @param port ポート番号 + */ + void setPort(int port); + + /** + * サーバを開始します. + * + * @param callback 開始結果を通知するコールバック + */ + void startWebServer(OnWebServerStartCallback callback); + + /** + * サーバを停止します. + */ + void stopWebServer(); + + /** + * 設定が変更されたことを通知します. + */ + void onConfigChange(); + + /** + * Recorder をミュート状態にする. + */ + void setMute(boolean mute); + + /** + * Recorder のミュート状態を返す. + * @return mute状態 + */ + boolean isMuted(); + + /** + * 映像のエンコーダーに対して sync frame の即時生成を要求する. + * + * @return 即時生成を受け付けた場合はtrue, そうでない場合はfalse + */ + boolean requestSyncFrame(); + + /** + * SSLContext を使用するかどうかのフラグを返す. + * + * @return SSLContext を使用する場合はtrue, そうでない場合はfalse + */ + boolean useSSLContext(); + + /** + * SSL コンテキストの設定を行います. + * + * @param sslContext SSL コンテキスト + */ + void setSSLContext(SSLContext sslContext); + + /** + * SSL コンテキストを取得します. + * + * @return SSL コンテキスト + */ + SSLContext getSSLContext(); + + /** + * プレビューサーバから配信したデータの BPS を取得します. + * + * @return プレビューサーバから配信したデータの BPS + */ + long getBPS(); + + /** + * Callback interface used to receive the result of starting a web server. + */ + interface OnWebServerStartCallback { + /** + * Called when a web server successfully started. + * + * @param uri An ever-updating, static image URI. + */ + void onStart(String uri); + + /** + * Called when a web server failed to start. + */ + void onFail(); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/PreviewServerProvider.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/PreviewServerProvider.java new file mode 100644 index 0000000000..8cb5e0751f --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/PreviewServerProvider.java @@ -0,0 +1,113 @@ +/* + PreviewServerProvider.java + Copyright (c) 2017 NTT DOCOMO,INC. + Released under the MIT license + http://opensource.org/licenses/mit-license.php + */ +package org.deviceconnect.android.deviceplugin.uvc.recorder; + +import java.util.List; + +public interface PreviewServerProvider { + /** + * プレビューで配信するマイムタイプを取得します. + * + * @return プレビューで配信するマイムタイプ + */ + List getSupportedMimeType(); + + /** + * サポートしているプレビュー配信サーバを追加します. + * + * @param server 追加するプレビュー配信サーバ + */ + void addServer(PreviewServer server); + + /** + * サポートしているプレビュー配信用サーバのリストを取得します. + * @return プレビュー配信用サーバのリスト + */ + List getServers(); + + /** + * 指定されたマイムタイプに対応するプレビュー配信サーバを取得します. + * + *

+ * マイムタイプに対応したプレビュー配信サーバが存在しない場合は null を返却します。 + *

+ * + * @param mimeType マイムタイプ + * @return プレビュー配信サーバ + */ + PreviewServer getServerByMimeType(String mimeType); + + /** + * プレビューサーバが動作している確認します. + * + * @return 動作中の場合は true、それ以外は false + */ + boolean isRunning(); + + /** + * 全てのプレビュー配信サーバを開始します. + * + * レスポンスのリストが空の場合には、全てのプレビュー配信サーバの起動に失敗しています。 + * + * @return 起動に成功したプレビュー配信サーバのリスト + */ + List startServers(); + + /** + * 全てのプレビュー配信サーバを停止します. + */ + void stopServers(); + + /** + * 全てのサーバの映像のエンコーダーに対して sync frame の即時生成を要求する. + * + * @return 実際に即時生成を受け付けたサーバのリスト + */ + List requestSyncFrame(); + + /** + * 設定が変更されたことを通知します. + */ + void onConfigChange(); + + /** + * Recorder をミュート状態にする. + */ + void setMute(boolean mute); + + /** + * イベントを通知するリスナーを設定します. + * + * @param listener リスナー + */ + void setOnEventListener(OnEventListener listener); + + /** + * プレビュー配信サーバのイベントを通知するリスナー. + */ + interface OnEventListener { + /** + * プレビュー配信サーバを開始したことを通知します. + * + * @param servers 開始したサーバのリスト + */ + void onStarted(List servers); + + /** + * プレビュー配信サーバを停止したことを通知します. + */ + void onStopped(); + + /** + * プレビュー配信サーバでエラーが発生したことを通知します. + * + * @param server エラーが発生したサーバ + * @param e エラー原因の例外 + */ + void onError(PreviewServer server, Exception e); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/UVCRecorder.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/UVCRecorder.java deleted file mode 100644 index a21eb06114..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/UVCRecorder.java +++ /dev/null @@ -1,195 +0,0 @@ -package org.deviceconnect.android.deviceplugin.uvc.recorder; - -import android.util.Log; - -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDevice; -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDeviceManager; -import org.deviceconnect.android.deviceplugin.uvc.recorder.preview.MJPEGPreviewServer; -import org.deviceconnect.android.deviceplugin.uvc.recorder.preview.PreviewServer; -import org.deviceconnect.android.deviceplugin.uvc.recorder.preview.RTSPPreviewServer; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import androidx.annotation.NonNull; - -public class UVCRecorder implements MediaRecorder { - - private static final String RECORDER_ID = "0"; - private static final String RECORDER_MIME_TYPE_MJPEG = "video/x-mjpeg"; - - private final List mPreviewServers = new ArrayList<>(); - private final UVCDeviceManager mDeviceMgr; - private UVCDevice mDevice; - /** - * コンストラクタ. - * @param manager ファイル管理クラス - * @param device UVCカメラ - */ - public UVCRecorder(final UVCDeviceManager manager, final UVCDevice device) { - mDeviceMgr = manager; - mDevice = device; - } - - public void setDevice(UVCDevice device) { - mDevice = device; - } - - @Override - public void initialize() { - mPreviewServers.clear(); - mPreviewServers.add(new MJPEGPreviewServer(false, mDeviceMgr, mDevice, 40000)); - mPreviewServers.add(new MJPEGPreviewServer(true, mDeviceMgr, mDevice, 41000)); - mPreviewServers.add(new RTSPPreviewServer(mDeviceMgr, mDevice, 40001)); - } - - @Override - public void clean() { - stopPreview(); - } - - @Override - public String getId() { - return RECORDER_ID; - } - - @Override - public String getName() { - return mDevice.getName(); - } - - @Override - public String getMimeType() { - return RECORDER_MIME_TYPE_MJPEG; - } - - @Override - public void setMimeType(String mimeType) { - } - - @Override - public State getState() { - return null; - } - - @Override - public Size getPictureSize() { - return null; - } - - @Override - public void setPictureSize(Size size) { - } - - @Override - public Size getPreviewSize() { - return new Size(mDevice.getPreviewWidth(), mDevice.getPreviewHeight()); - } - - @Override - public void setPreviewSize(Size size) { - mDevice.setPreviewSize(size.getWidth(), size.getHeight()); - } - - @Override - public double getMaxFrameRate() { - return mDevice.getFrameRate(); - } - - @Override - public void setMaxFrameRate(double frameRate) { - mDevice.setPreviewFrameRate(frameRate); - } - - @Override - public int getPreviewBitRate() { - return 0; - } - - @Override - public void setPreviewBitRate(int bitRate) { - } - - @Override - public List getSupportedPictureSizes() { - List result = new ArrayList<>(); - for (UVCDevice.PreviewOption option : mDevice.getPreviewOptions()) { - result.add(new Size(option.getWidth(), option.getHeight())); - } - return result; - } - - @Override - public List getSupportedPreviewSizes() { - List result = new ArrayList<>(); - for (UVCDevice.PreviewOption option : mDevice.getPreviewOptions()) { - result.add(new Size(option.getWidth(), option.getHeight())); - } - return result; - } - - @Override - public List getSupportedMimeTypes() { - List result = new ArrayList<>(); - for (PreviewServer server : mPreviewServers) { - result.add(server.getMimeType()); - } - return result; - } - - @Override - public void takePhoto(OnPhotoEventListener listener) { - } - - @Override - public List getServers() { - return mPreviewServers; - } - - @Override - public List startPreview() { - List results = new ArrayList<>(); - - CountDownLatch lock = new CountDownLatch(mPreviewServers.size()); - for (PreviewServer server : mPreviewServers) { - server.start(new PreviewServer.OnWebServerStartCallback() { - @Override - public void onStart(@NonNull String uri) { - results.add(server); - lock.countDown(); - } - - @Override - public void onFail() { - lock.countDown(); - } - }); - } - try { - lock.await(5, TimeUnit.SECONDS); - } catch (Exception e) { - // ignore. - } - - return results; - } - - @Override - public void stopPreview() { - for (PreviewServer server : mPreviewServers) { - server.stop(); - } - } - - @Override - public boolean isStartedPreview() { - for (PreviewServer server : mPreviewServers) { - if (server.isStarted()) { - return true; - } - } - return false; - } -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/h264/UvcH264Recorder.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/h264/UvcH264Recorder.java new file mode 100644 index 0000000000..c4284f89f1 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/h264/UvcH264Recorder.java @@ -0,0 +1,148 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.h264; + +import android.content.Context; +import android.util.Size; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.uvc.UvcRecorder; +import org.deviceconnect.android.libuvc.FrameType; +import org.deviceconnect.android.libuvc.Parameter; +import org.deviceconnect.android.libuvc.UVCCamera; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class UvcH264Recorder extends UvcRecorder { + private static final String RECORDER_ID = "1"; + private static final String RECORDER_NAME = "h264"; + private static final String RECORDER_MIME_TYPE_MJPEG = "video/x-mjpeg"; + + public UvcH264Recorder(Context context, UVCCamera camera) { + super(context, camera); + } + + @Override + protected UvcSettings createSettings() { + H264Settings settings = new H264Settings(getContext()); + if (!settings.isInitialized()) { + List supportPictureSizes = settings.getSupportedPictureSizes(); + List supportPreviewSizes = settings.getSupportedPreviewSizes(); + + settings.setPictureSize(supportPictureSizes.get(0)); + settings.setPreviewSize(supportPreviewSizes.get(0)); + settings.setPreviewBitRate(2 * 1024 * 1024); + settings.setPreviewMaxFrameRate(30); + settings.setPreviewKeyFrameInterval(1); + settings.setPreviewQuality(80); + + settings.setPreviewAudioSource(null); + settings.setPreviewAudioBitRate(64 * 1024); + settings.setPreviewSampleRate(16000); + settings.setPreviewChannel(1); + settings.setUseAEC(true); + + settings.setMjpegPort(11001); + settings.setMjpegSSLPort(11101); + settings.setRtspPort(12001); + settings.setSrtPort(13001); + + settings.finishInitialization(); + } + return settings; + } + + @Override + public String getId() { + return RECORDER_ID; + } + + @Override + public String getName() { + return RECORDER_NAME; + } + + @Override + public String getMimeType() { + return RECORDER_MIME_TYPE_MJPEG; + } + + public class H264Settings extends UvcSettings { + H264Settings(Context context) { + super(context); + } + + @Override + public List getSupportedPictureSizes() { + List sizes = new ArrayList<>(); + try { + List parameters = getUVCCamera().getParameter(); + for (Parameter p : parameters) { + if (p.getFrameType() == FrameType.H264) { + sizes.add(new Size(p.getWidth(), p.getHeight())); + } else if (p.getFrameType() == FrameType.MJPEG) { + if (p.hasExtH264()) { + sizes.add(new Size(p.getWidth(), p.getHeight())); + } + } + } + } catch (IOException e) { + // ignore. + } + return sizes; + } + + @Override + public List getSupportedPreviewSizes() { + List sizes = new ArrayList<>(); + try { + List parameters = getUVCCamera().getParameter(); + for (Parameter p : parameters) { + if (p.getFrameType() == FrameType.H264) { + sizes.add(new Size(p.getWidth(), p.getHeight())); + } else if (p.getFrameType() == FrameType.MJPEG) { + if (p.hasExtH264()) { + sizes.add(new Size(p.getWidth(), p.getHeight())); + } + } + } + } catch (IOException e) { + // ignore. + } + return sizes; + } + + /** + * 設定されているパラメータに一致するパラメータを取得します. + * + * @return パラメータ + * @throws IOException カメラ情報の取得に失敗した場合に例外が発生 + */ + public Parameter getParameter() throws IOException { + Parameter parameter = null; + Size previewSize = getPreviewSize(); + List parameters = getUVCCamera().getParameter(); + for (Parameter p : parameters) { + if (p.getFrameType() == FrameType.H264) { + if (p.getWidth() == previewSize.getWidth() && p.getHeight() == previewSize.getHeight()) { + parameter = p; + break; + } + } + } + + if (parameter == null) { + for (Parameter p : parameters) { + if (p.getFrameType() == FrameType.MJPEG && p.hasExtH264()) { + if (p.getWidth() == previewSize.getWidth() && p.getHeight() == previewSize.getHeight()) { + parameter = p; + parameter.setUseH264(true); + break; + } + } + } + } + + return parameter; + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/mjpeg/UvcMjpgRecorder.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/mjpeg/UvcMjpgRecorder.java new file mode 100644 index 0000000000..1ef28ddd9f --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/mjpeg/UvcMjpgRecorder.java @@ -0,0 +1,128 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.mjpeg; + +import android.content.Context; +import android.util.Size; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.uvc.UvcRecorder; +import org.deviceconnect.android.libuvc.FrameType; +import org.deviceconnect.android.libuvc.Parameter; +import org.deviceconnect.android.libuvc.UVCCamera; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class UvcMjpgRecorder extends UvcRecorder { + private static final String RECORDER_ID = "0"; + private static final String RECORDER_NAME = "mjpeg"; + private static final String RECORDER_MIME_TYPE_MJPEG = "video/x-mjpeg"; + + public UvcMjpgRecorder(Context context, UVCCamera camera) { + super(context, camera); + } + + @Override + protected UvcSettings createSettings() { + MjpegSettings settings = new MjpegSettings(getContext()); + if (!settings.isInitialized()) { + List supportPictureSizes = settings.getSupportedPictureSizes(); + List supportPreviewSizes = settings.getSupportedPreviewSizes(); + + settings.setPictureSize(supportPictureSizes.get(0)); + settings.setPreviewSize(supportPreviewSizes.get(0)); + + settings.setPreviewBitRate(2 * 1024 * 1024); + settings.setPreviewMaxFrameRate(30); + settings.setPreviewKeyFrameInterval(1); + settings.setPreviewQuality(80); + + settings.setPreviewAudioSource(null); + settings.setPreviewAudioBitRate(64 * 1024); + settings.setPreviewSampleRate(16000); + settings.setPreviewChannel(1); + settings.setUseAEC(true); + + settings.setMjpegPort(11000); + settings.setMjpegSSLPort(11100); + settings.setRtspPort(12000); + settings.setSrtPort(13000); + + settings.finishInitialization(); + } + return settings; + } + + @Override + public String getId() { + return RECORDER_ID; + } + + @Override + public String getName() { + return RECORDER_NAME; + } + + @Override + public String getMimeType() { + return RECORDER_MIME_TYPE_MJPEG; + } + + public class MjpegSettings extends UvcSettings { + MjpegSettings(Context context) { + super(context); + } + + @Override + public List getSupportedPictureSizes() { + List sizes = new ArrayList<>(); + try { + List parameters = getUVCCamera().getParameter(); + for (Parameter p : parameters) { + if (p.getFrameType() == FrameType.MJPEG) { + sizes.add(new Size(p.getWidth(), p.getHeight())); + } + } + } catch (IOException e) { + // ignore. + } + return sizes; + } + + @Override + public List getSupportedPreviewSizes() { + List sizes = new ArrayList<>(); + try { + List parameters = getUVCCamera().getParameter(); + for (Parameter p : parameters) { + if (p.getFrameType() == FrameType.MJPEG) { + sizes.add(new Size(p.getWidth(), p.getHeight())); + } + } + } catch (IOException e) { + // ignore. + } + return sizes; + } + + /** + * 設定されているパラメータに一致するパラメータを取得します. + * + * @return パラメータ + * @throws IOException カメラ情報の取得に失敗した場合に例外が発生 + */ + public Parameter getParameter() throws IOException { + Parameter parameter = null; + Size previewSize = getPreviewSize(); + List parameters = getUVCCamera().getParameter(); + for (Parameter p : parameters) { + if (p.getFrameType() == FrameType.MJPEG) { + if (p.getWidth() == previewSize.getWidth() && p.getHeight() == previewSize.getHeight()) { + parameter = p; + parameter.setUseH264(false); + } + } + } + return parameter; + } + } +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/MJPEGPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/MJPEGPreviewServer.java deleted file mode 100644 index 11ced68a0c..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/MJPEGPreviewServer.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.deviceconnect.android.deviceplugin.uvc.recorder.preview; - -import com.serenegiant.usb.UVCCamera; - -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDevice; -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDeviceManager; -import org.deviceconnect.android.libmedia.streaming.mjpeg.MJPEGEncoder; -import org.deviceconnect.android.libmedia.streaming.mjpeg.MJPEGServer; - -import java.io.IOException; -import java.net.Socket; - -import javax.net.ssl.SSLContext; - -public class MJPEGPreviewServer implements PreviewServer { - - private static final String SERVER_NAME = "UVC Plugin MotionJPEG Server"; - - private final UVCDeviceManager mDeviceMgr; - private final UVCDevice mDevice; - private MJPEGServer mServer; - protected int mPort; - /** - * SSLContext を使用するかどうかのフラグ. - */ - private boolean mUsesSSLContext; - /** - * SSLContext のインスタンス. - */ - private SSLContext mSSLContext; - public MJPEGPreviewServer(final boolean isSSL, final UVCDeviceManager mgr, final UVCDevice device, final int port) { - mDeviceMgr = mgr; - mDevice = device; - mPort = port; - mUsesSSLContext = isSSL; - } - - @Override - public String getUrl() { - return mServer == null ? null : mServer.getUri(); - } - - @Override - public String getMimeType() { - return "video/x-mjpeg"; - } - - @Override - public boolean isStarted() { - return mServer != null; - } - - @Override - public boolean usesSSLContext() { - return mUsesSSLContext; - } - - @Override - public void setSSLContext(SSLContext sslContext) { - mSSLContext = sslContext; - } - - @Override - public SSLContext getSSLContext() { - return mSSLContext; - } - - @Override - public void start(final OnWebServerStartCallback callback) { - if (mServer == null) { - SSLContext sslContext = getSSLContext(); - if (usesSSLContext() && sslContext == null) { - callback.onFail(); - return; - } - mServer = new MJPEGServer(); - if (sslContext != null) { - mServer.setSSLContext(sslContext); - } - mServer.setServerName(SERVER_NAME); - mServer.setServerPort(mPort); - mServer.setCallback(mCallback); - try { - mServer.start(); - } catch (IOException e) { - callback.onFail(); - return; - } - } - callback.onStart(mServer.getUri()); - } - - @Override - public void stop() { - if (mServer != null) { - mServer.stop(); - mServer = null; - } - } - - protected MJPEGServer.Callback mCallback = new MJPEGServer.Callback() { - @Override - public boolean onAccept(Socket socket) { - return true; - } - - @Override - public void onClosed(Socket socket) { - } - - @Override - public MJPEGEncoder createMJPEGEncoder() { - return new UVCEncoder(); - } - - @Override - public void releaseMJPEGEncoder(MJPEGEncoder encoder) { - } - }; - - private class UVCEncoder extends MJPEGEncoder implements UVCDeviceManager.PreviewListener { - @Override - public void start() { - boolean isStarted = mDevice.startPreview(); - if (isStarted) { - mDeviceMgr.addPreviewListener(this); - } - } - - @Override - public void stop() { - mDeviceMgr.removePreviewListener(this); - } - - // UVCDeviceManager.PreviewListener - - @Override - public void onFrame(UVCDevice device, byte[] frame, int frameFormat, int width, int height) { - if (frameFormat != UVCCamera.FRAME_FORMAT_MJPEG) { - return; - } - postJPEG(frame); - } - } -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/PreviewServer.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/PreviewServer.java deleted file mode 100644 index 1119cc7a6b..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/PreviewServer.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - PreviewServer.java - Copyright (c) 2017 NTT DOCOMO,INC. - Released under the MIT license - http://opensource.org/licenses/mit-license.php - */ -package org.deviceconnect.android.deviceplugin.uvc.recorder.preview; - - -import javax.net.ssl.SSLContext; - -public interface PreviewServer { - - String getUrl(); - - String getMimeType(); - - /** - * サーバを開始します. - * @param callback 開始結果を通知するコールバック - */ - void start(OnWebServerStartCallback callback); - - /** - * サーバを停止します. - */ - void stop(); - - /** - * サーバが開始されているか確認します. - * - * @return サーバが開始されている場合はtrue、それ以外はfalse - */ - boolean isStarted(); - /** - * SSLContext を使用するかどうかのフラグを返す. - * - * @return SSLContext を使用する場合はtrue, そうでない場合はfalse - */ - boolean usesSSLContext(); - - void setSSLContext(SSLContext sslContext); - - SSLContext getSSLContext(); - /** - * Callback interface used to receive the result of starting a web server. - */ - interface OnWebServerStartCallback { - /** - * Called when a web server successfully started. - * - * @param uri An ever-updating, static image URI. - */ - void onStart(String uri); - - /** - * Called when a web server failed to start. - */ - void onFail(); - } -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/PreviewServerProvider.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/PreviewServerProvider.java deleted file mode 100644 index 31c082a936..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/PreviewServerProvider.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.deviceconnect.android.deviceplugin.uvc.recorder.preview; - - -import java.util.List; - -public interface PreviewServerProvider { - - List getServers(); - - void stopAll(); -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/RTSPPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/RTSPPreviewServer.java deleted file mode 100644 index 709fcf662e..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/preview/RTSPPreviewServer.java +++ /dev/null @@ -1,177 +0,0 @@ -package org.deviceconnect.android.deviceplugin.uvc.recorder.preview; - - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Paint; - -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDevice; -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDeviceManager; -import org.deviceconnect.android.libmedia.streaming.rtsp.RtspServer; -import org.deviceconnect.android.libmedia.streaming.rtsp.session.RtspSession; -import org.deviceconnect.android.libmedia.streaming.rtsp.session.video.H264VideoStream; -import org.deviceconnect.android.libmedia.streaming.video.CanvasVideoEncoder; -import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; -import org.deviceconnect.android.libmedia.streaming.video.VideoQuality; - -import java.io.IOException; - -import javax.net.ssl.SSLContext; - -public class RTSPPreviewServer implements PreviewServer { - - private static final String SERVER_NAME = "UVC Plugin RTSP Server"; - - private RtspServer mServer; - private UVCDeviceManager mDeviceMgr; - private UVCDevice mDevice; - private int mPort; - - public RTSPPreviewServer(final UVCDeviceManager mgr, final UVCDevice device, final int port) { - mDeviceMgr = mgr; - mDevice = device; - mPort = port; - } - - @Override - public String getUrl() { - return "rtsp://localhost:" + mPort; - } - - @Override - public String getMimeType() { - return "video/x-rtp"; - } - - @Override - public boolean isStarted() { - return mServer != null; - } - - @Override - public boolean usesSSLContext() { - return false; - } - - @Override - public void setSSLContext(SSLContext sslContext) { - - } - - @Override - public SSLContext getSSLContext() { - return null; - } - - @Override - public void start(final OnWebServerStartCallback callback) { - if (mServer == null) { - mServer = new RtspServer(); - mServer.setServerName(SERVER_NAME); - mServer.setServerPort(mPort); - mServer.setCallback(mCallback); - - try { - mServer.start(); - } catch (IOException e) { - callback.onFail(); - return; - } - } - - callback.onStart(getUrl()); - } - - @Override - public void stop() { - if (mServer != null) { - mServer.stop(); - mServer = null; - } - } - - private final RtspServer.Callback mCallback = new RtspServer.Callback() { - @Override - public void createSession(RtspSession session) { - UVCStream uvcStream = new UVCStream(); - - VideoQuality videoQuality = uvcStream.getVideoEncoder().getVideoQuality(); - videoQuality.setVideoWidth(mDevice.getPreviewWidth()); - videoQuality.setVideoHeight(mDevice.getPreviewHeight()); - videoQuality.setBitRate(1024 * 1024); - videoQuality.setFrameRate((int) mDevice.getFrameRate()); - videoQuality.setIFrameInterval(2); - - session.setVideoMediaStream(uvcStream); - } - - @Override - public void releaseSession(RtspSession session) { - - } - }; - - private class UVCStream extends H264VideoStream { - private UVCEncoder mUVCEncoder; - - UVCStream() { - mUVCEncoder = new UVCEncoder(); - } - - @Override - public VideoEncoder getVideoEncoder() { - return mUVCEncoder; - } - } - - private class UVCEncoder extends CanvasVideoEncoder implements UVCDeviceManager.PreviewListener { - private Bitmap mBitmap; - private Paint mPaint = new Paint(); - - void setBitmap(Bitmap bitmap) { - synchronized (this) { - if (mBitmap != null) { - mBitmap.recycle(); - mBitmap = null; - } - mBitmap = bitmap; - } - } - - @Override - protected void onStartSurfaceDrawing() { - super.onStartSurfaceDrawing(); - - if (!mDevice.startPreview()) { - // TODO - } - mDeviceMgr.addPreviewListener(this); - } - - @Override - protected void onStopSurfaceDrawing() { - mDevice.stopPreview(); - mDeviceMgr.removePreviewListener(this); - - super.onStopSurfaceDrawing(); - } - - @Override - public void draw(Canvas canvas, int width, int height) { - synchronized (this) { - if (mBitmap != null && !mBitmap.isRecycled()) { - canvas.drawBitmap(mBitmap, 0, 0, mPaint); - } - } - } - - @Override - public void onFrame(UVCDevice device, byte[] frame, int frameFormat, int width, int height) { - Bitmap bitmap = BitmapFactory.decodeByteArray(frame, 0, frame.length); - if (bitmap != null) { - setBitmap(bitmap); - } - } - } -} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uncompressed/UvcUncompressedRecorder.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uncompressed/UvcUncompressedRecorder.java new file mode 100644 index 0000000000..e4cb12a9f7 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uncompressed/UvcUncompressedRecorder.java @@ -0,0 +1,128 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uncompressed; + +import android.content.Context; +import android.util.Size; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.uvc.UvcRecorder; +import org.deviceconnect.android.libuvc.FrameType; +import org.deviceconnect.android.libuvc.Parameter; +import org.deviceconnect.android.libuvc.UVCCamera; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class UvcUncompressedRecorder extends UvcRecorder { + private static final String RECORDER_ID = "2"; + private static final String RECORDER_NAME = "uncompressed"; + private static final String RECORDER_MIME_TYPE_MJPEG = "video/x-mjpeg"; + + public UvcUncompressedRecorder(Context context, UVCCamera camera) { + super(context, camera); + } + + @Override + protected UvcSettings createSettings() { + UncompressedSettings settings = new UncompressedSettings(getContext()); + if (!settings.isInitialized()) { + List supportPictureSizes = settings.getSupportedPictureSizes(); + List supportPreviewSizes = settings.getSupportedPreviewSizes(); + + settings.setPictureSize(supportPictureSizes.get(0)); + settings.setPreviewSize(supportPreviewSizes.get(0)); + + settings.setPreviewBitRate(2 * 1024 * 1024); + settings.setPreviewMaxFrameRate(30); + settings.setPreviewKeyFrameInterval(1); + settings.setPreviewQuality(80); + + settings.setPreviewAudioSource(null); + settings.setPreviewAudioBitRate(64 * 1024); + settings.setPreviewSampleRate(16000); + settings.setPreviewChannel(1); + settings.setUseAEC(true); + + settings.setMjpegPort(11002); + settings.setMjpegSSLPort(11102); + settings.setRtspPort(12002); + settings.setSrtPort(13002); + + settings.finishInitialization(); + } + return settings; + } + + @Override + public String getId() { + return RECORDER_ID; + } + + @Override + public String getName() { + return RECORDER_NAME; + } + + @Override + public String getMimeType() { + return RECORDER_MIME_TYPE_MJPEG; + } + + public class UncompressedSettings extends UvcSettings { + UncompressedSettings(Context context) { + super(context); + } + + @Override + public List getSupportedPictureSizes() { + List sizes = new ArrayList<>(); + try { + List parameters = getUVCCamera().getParameter(); + for (Parameter p : parameters) { + if (p.getFrameType() == FrameType.UNCOMPRESSED) { + sizes.add(new Size(p.getWidth(), p.getHeight())); + } + } + } catch (IOException e) { + // ignore. + } + return sizes; + } + + @Override + public List getSupportedPreviewSizes() { + List sizes = new ArrayList<>(); + try { + List parameters = getUVCCamera().getParameter(); + for (Parameter p : parameters) { + if (p.getFrameType() == FrameType.UNCOMPRESSED) { + sizes.add(new Size(p.getWidth(), p.getHeight())); + } + } + } catch (IOException e) { + // ignore. + } + return sizes; + } + + /** + * 設定されているパラメータに一致するパラメータを取得します. + * + * @return パラメータ + * @throws IOException カメラ情報の取得に失敗した場合に例外が発生 + */ + public Parameter getParameter() throws IOException { + Parameter parameter = null; + Size previewSize = getPreviewSize(); + List parameters = getUVCCamera().getParameter(); + for (Parameter p : parameters) { + if (p.getFrameType() == FrameType.UNCOMPRESSED) { + if (p.getWidth() == previewSize.getWidth() && p.getHeight() == previewSize.getHeight()) { + parameter = p; + parameter.setUseH264(false); + } + } + } + return parameter; + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcBroadcasterProvider.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcBroadcasterProvider.java new file mode 100644 index 0000000000..54574a0ee8 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcBroadcasterProvider.java @@ -0,0 +1,29 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import android.content.Context; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.AbstractBroadcastProvider; +import org.deviceconnect.android.deviceplugin.uvc.recorder.Broadcaster; + +public class UvcBroadcasterProvider extends AbstractBroadcastProvider { + /** + * カメラを操作するレコーダ. + */ + private final UvcRecorder mRecorder; + + public UvcBroadcasterProvider(Context context, UvcRecorder recorder) { + super(context, recorder); + mRecorder = recorder; + } + + @Override + public Broadcaster createBroadcaster(String broadcastURI) { + if (broadcastURI.startsWith("srt://")) { + return new UvcSRTBroadcaster(mRecorder, broadcastURI); + } else if (broadcastURI.startsWith("rtmp://") || broadcastURI.startsWith("rtmps://")) { + return new UvcRTMPBroadcaster(mRecorder, broadcastURI); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcH264VideoStream.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcH264VideoStream.java new file mode 100644 index 0000000000..5892c1ae46 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcH264VideoStream.java @@ -0,0 +1,28 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import org.deviceconnect.android.libmedia.streaming.rtsp.session.video.H264VideoStream; +import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; + +public class UvcH264VideoStream extends H264VideoStream { + /** + * 映像用エンコーダ. + */ + private final VideoEncoder mVideoEncoder; + + /** + * コンストラクタ. + * コンストラクタ. + * + * @param recorder 操作するカメラのレコーダ. + * @param port 送信先のポート番号 + */ + UvcH264VideoStream(UvcRecorder recorder, int port) { + mVideoEncoder = new UvcVideoEncoder(recorder); + setDestinationPort(port); + } + + @Override + public VideoEncoder getVideoEncoder() { + return mVideoEncoder; + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcH264toMJPEGEncoder.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcH264toMJPEGEncoder.java new file mode 100644 index 0000000000..5adbe2b26c --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcH264toMJPEGEncoder.java @@ -0,0 +1,28 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import org.deviceconnect.android.libmedia.streaming.mjpeg.SurfaceMJPEGEncoder; + +public class UvcH264toMJPEGEncoder extends SurfaceMJPEGEncoder { + + public UvcH264toMJPEGEncoder(UvcRecorder recorder) { + super(recorder.getSurfaceDrawingThread()); + } + + // SurfaceMJPEGEncoder + + @Override + protected void prepare() { + } + + @Override + protected void startRecording() { + } + + @Override + protected void stopRecording() { + } + + @Override + protected void release() { + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcH265VideoStream.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcH265VideoStream.java new file mode 100644 index 0000000000..a61bf84f2a --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcH265VideoStream.java @@ -0,0 +1,27 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import org.deviceconnect.android.libmedia.streaming.rtsp.session.video.H265VideoStream; +import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; + +public class UvcH265VideoStream extends H265VideoStream { + /** + * 映像用エンコーダ. + */ + private final VideoEncoder mVideoEncoder; + + /** + * コンストラクタ. + * + * @param recorder 操作するカメラのレコーダ. + * @param port 送信先のポート番号 + */ + UvcH265VideoStream(UvcRecorder recorder, int port) { + mVideoEncoder = new UvcVideoEncoder(recorder, "video/hevc"); + setDestinationPort(port); + } + + @Override + public VideoEncoder getVideoEncoder() { + return mVideoEncoder; + } +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcMJPEGEncoder.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcMJPEGEncoder.java new file mode 100644 index 0000000000..505914858d --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcMJPEGEncoder.java @@ -0,0 +1,46 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import org.deviceconnect.android.libmedia.streaming.mjpeg.MJPEGEncoder; +import org.deviceconnect.android.libuvc.Parameter; + +import java.io.IOException; + +public class UvcMJPEGEncoder extends MJPEGEncoder { + + private final UvcRecorder mRecorder; + + public UvcMJPEGEncoder(UvcRecorder recorder) { + mRecorder = recorder; + } + + @Override + public void start() { + mRecorder.getUVCCamera().setPreviewCallback((frame) -> { + try { + postJPEG(frame.getBuffer()); + } finally { + frame.release(); + } + }); + + try { + UvcRecorder.UvcSettings settings = (UvcRecorder.UvcSettings) mRecorder.getSettings(); + Parameter p = settings.getParameter(); + if (p == null) { + throw new RuntimeException("UVC parameter not found."); + } + mRecorder.getUVCCamera().startVideo(p); + } catch (Exception e) { + throw new RuntimeException(); + } + } + + @Override + public void stop() { + try { + mRecorder.getUVCCamera().stopVideo(); + } catch (IOException e) { + // ignore. + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcMJPEGPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcMJPEGPreviewServer.java new file mode 100644 index 0000000000..aaf8ba5d5d --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcMJPEGPreviewServer.java @@ -0,0 +1,34 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import android.content.Context; +import android.util.Log; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.AbstractMJPEGPreviewServer; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.h264.UvcH264Recorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.mjpeg.UvcMjpgRecorder; +import org.deviceconnect.android.libmedia.streaming.mjpeg.MJPEGEncoder; + +public class UvcMJPEGPreviewServer extends AbstractMJPEGPreviewServer { + UvcMJPEGPreviewServer(Context context, UvcRecorder recorder, int port, boolean useSSL) { + super(context, recorder, useSSL); + setPort(port); + } + + @Override + protected MJPEGEncoder createSurfaceMJPEGEncoder() { + // UvcMJPEGEncoder を使用すると UVC から送られてくる JPEG を + // そのまま MJPEG サーバから配信しますが、それだと端末の画面に + // 表示する処理が行えないので、MJPEG を一旦 Surface に描画してからエンコードするようにします。 + return new UvcH264toMJPEGEncoder((UvcRecorder) getRecorder()); + // 以下の条件分を使用することで、MJPEG をそのまま配信するようになります。 +// MediaRecorder recorder = getRecorder(); +// if (recorder instanceof UvcH264Recorder) { +// return new UvcH264toMJPEGEncoder((UvcRecorder) getRecorder()); +// } else if (recorder instanceof UvcMjpgRecorder) { +// return new UvcMJPEGEncoder((UvcRecorder) getRecorder()); +// } else { +// return null; +// } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcPreviewServerProvider.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcPreviewServerProvider.java new file mode 100644 index 0000000000..3b390f2915 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcPreviewServerProvider.java @@ -0,0 +1,25 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import android.content.Context; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.AbstractPreviewServerProvider; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; + +public class UvcPreviewServerProvider extends AbstractPreviewServerProvider { + /** + * コンストラクタ. + * + * @param context コンテキスト + * @param recorder レコーダ + */ + public UvcPreviewServerProvider(final Context context, final UvcRecorder recorder) { + super(context, recorder); + + MediaRecorder.Settings settings = recorder.getSettings(); + + addServer(new UvcMJPEGPreviewServer(context, recorder, settings.getMjpegPort(), false)); + addServer(new UvcMJPEGPreviewServer(context, recorder, settings.getMjpegSSLPort(), true)); + addServer(new UvcRTSPPreviewServer(context, recorder, settings.getRtspPort())); + addServer(new UvcSRTPreviewServer(context, recorder, settings.getSrtPort())); + } +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcRTMPBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcRTMPBroadcaster.java new file mode 100644 index 0000000000..fbb3a0eb5d --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcRTMPBroadcaster.java @@ -0,0 +1,17 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.AbstractRTMPBroadcaster; +import org.deviceconnect.android.deviceplugin.uvc.recorder.h264.UvcH264Recorder; +import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; + +public class UvcRTMPBroadcaster extends AbstractRTMPBroadcaster { + + public UvcRTMPBroadcaster(UvcRecorder recorder, String broadcastURI) { + super(recorder, broadcastURI); + } + + @Override + protected VideoEncoder createVideoEncoder() { + return new UvcVideoEncoder((UvcRecorder) getRecorder()); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcRTSPPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcRTSPPreviewServer.java new file mode 100644 index 0000000000..37311b75ff --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcRTSPPreviewServer.java @@ -0,0 +1,29 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import android.content.Context; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.AbstractRTSPPreviewServer; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.h264.UvcH264Recorder; +import org.deviceconnect.android.libmedia.streaming.rtsp.session.video.VideoStream; + +public class UvcRTSPPreviewServer extends AbstractRTSPPreviewServer { + + UvcRTSPPreviewServer(Context context, UvcRecorder recorder, int port) { + super(context, recorder); + setPort(port); + } + + @Override + protected VideoStream createVideoStream() { + UvcRecorder recorder = (UvcRecorder) getRecorder(); + MediaRecorder.Settings settings = recorder.getSettings(); + switch (settings.getPreviewEncoderName()) { + case H264: + default: + return new UvcH264VideoStream(recorder, 5006); + case H265: + return new UvcH265VideoStream(recorder, 5006); + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcRecorder.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcRecorder.java new file mode 100644 index 0000000000..6937d714dd --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcRecorder.java @@ -0,0 +1,88 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import android.Manifest; +import android.content.Context; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.AbstractMediaRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.BroadcasterProvider; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.PreviewServerProvider; +import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; +import org.deviceconnect.android.libuvc.Parameter; +import org.deviceconnect.android.libuvc.UVCCamera; + +import java.io.IOException; + +public abstract class UvcRecorder extends AbstractMediaRecorder { + private final UVCCamera mUVCCamera; + private final UvcSettings mSettings; + private final UvcSurfaceDrawingThread mUvcSurfaceDrawingThread; + private final UvcBroadcasterProvider mUvcBroadcasterProvider; + private final UvcPreviewServerProvider mUvcPreviewServerProvider; + + public UvcRecorder(Context context, UVCCamera camera) { + super(context); + + if (camera == null) { + throw new IllegalArgumentException("UVCCamera is null."); + } + + mUVCCamera = camera; + mSettings = createSettings(); + mUvcSurfaceDrawingThread = new UvcSurfaceDrawingThread(this); + mUvcBroadcasterProvider = new UvcBroadcasterProvider(context, this); + mUvcPreviewServerProvider = new UvcPreviewServerProvider(context, this); + } + + public UVCCamera getUVCCamera() { + return mUVCCamera; + } + + protected abstract UvcSettings createSettings(); + + @Override + public Settings getSettings() { + return mSettings; + } + + @Override + public BroadcasterProvider getBroadcasterProvider() { + return mUvcBroadcasterProvider; + } + + @Override + public PreviewServerProvider getServerProvider() { + return mUvcPreviewServerProvider; + } + + @Override + public EGLSurfaceDrawingThread getSurfaceDrawingThread() { + return mUvcSurfaceDrawingThread; + } + + @Override + public void requestPermission(MediaRecorder.PermissionCallback callback) { + requestPermission(new String[] {Manifest.permission.CAMERA}, callback); + } + + public String getSettingsName() { + return mUVCCamera.getDeviceId() + "-" + getId(); + } + + public class UvcSettings extends MediaRecorder.Settings { + + public UvcSettings(Context context) { + super(context, getSettingsName()); + } + + /** + * 設定されているパラメータに一致するパラメータを取得します. + * + * @return パラメータ + * @throws IOException カメラ情報の取得に失敗した場合に例外が発生 + */ + public Parameter getParameter() throws IOException { + return null; + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcSRTBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcSRTBroadcaster.java new file mode 100644 index 0000000000..f39145e753 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcSRTBroadcaster.java @@ -0,0 +1,25 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.AbstractSRTBroadcaster; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; +import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; + +public class UvcSRTBroadcaster extends AbstractSRTBroadcaster { + + public UvcSRTBroadcaster(UvcRecorder recorder, String broadcastURI) { + super(recorder, broadcastURI); + } + + @Override + protected VideoEncoder createVideoEncoder() { + UvcRecorder recorder = (UvcRecorder) getRecorder(); + MediaRecorder.Settings settings = recorder.getSettings(); + switch (settings.getPreviewEncoderName()) { + case H264: + default: + return new UvcVideoEncoder(recorder); + case H265: + return new UvcVideoEncoder(recorder, "video/hevc"); + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcSRTPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcSRTPreviewServer.java new file mode 100644 index 0000000000..a50aaa7656 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcSRTPreviewServer.java @@ -0,0 +1,28 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import android.content.Context; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.AbstractSRTPreviewServer; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.h264.UvcH264Recorder; +import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; + +public class UvcSRTPreviewServer extends AbstractSRTPreviewServer { + UvcSRTPreviewServer(final Context context, final UvcRecorder recorder, final int port) { + super(context, recorder); + setPort(port); + } + + @Override + protected VideoEncoder createVideoEncoder() { + UvcRecorder recorder = (UvcRecorder) getRecorder(); + MediaRecorder.Settings settings = recorder.getSettings(); + switch (settings.getPreviewEncoderName()) { + case H264: + default: + return new UvcVideoEncoder(recorder); + case H265: + return new UvcVideoEncoder(recorder, "video/hevc"); + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcSurfaceDrawingThread.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcSurfaceDrawingThread.java new file mode 100644 index 0000000000..2505fc6cad --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcSurfaceDrawingThread.java @@ -0,0 +1,113 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import android.graphics.SurfaceTexture; +import android.util.Log; +import android.util.Size; +import android.view.Surface; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; +import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; +import org.deviceconnect.android.libuvc.Parameter; +import org.deviceconnect.android.libuvc.UVCCamera; +import org.deviceconnect.android.libuvc.player.UVCPlayer; +import org.deviceconnect.android.libuvc.player.UVCPlayerException; + +public class UvcSurfaceDrawingThread extends EGLSurfaceDrawingThread { + /** + * レコーダ. + */ + private final UvcRecorder mRecorder; + + /** + * UVC からの映像をデコードするためのプレイヤー. + */ + private UVCPlayer mPlayer; + + public UvcSurfaceDrawingThread(UvcRecorder recorder) { + if (recorder == null) { + throw new IllegalArgumentException("recorder is null."); + } + mRecorder = recorder; + } + + public UvcRecorder getRecorder() { + return mRecorder; + } + + // EGLSurfaceDrawingThread + + @Override + public void start() { + MediaRecorder.Settings settings = mRecorder.getSettings(); + Size previewSize = settings.getPreviewSize(); + setSize(previewSize.getWidth(), previewSize.getHeight()); + setDrawingRange(settings.getDrawingRange()); + super.start(); + } + + @Override + public int getDisplayRotation() { + return Surface.ROTATION_0; + } + + @Override + public boolean isSwappedDimensions() { + return false; + } + + @Override + protected void onStarted() { + startCamera(getSurfaceTexture()); + } + + @Override + protected void onStopped() { + stopCamera(); + } + + private synchronized void startCamera(SurfaceTexture surfaceTexture) { + try { + UvcRecorder.UvcSettings settings = (UvcRecorder.UvcSettings) mRecorder.getSettings(); + + UVCCamera camera = mRecorder.getUVCCamera(); + if (camera.isRunning()) { + throw new RuntimeException("UVC camera is already running."); + } + + Parameter parameter = settings.getParameter(); + if (parameter == null) { + throw new RuntimeException("UVC parameter not found."); + } + + mPlayer = new UVCPlayer(); + mPlayer.setSurface(new Surface(surfaceTexture)); + mPlayer.setOnEventListener(new UVCPlayer.OnEventListener() { + @Override + public void onStarted() { + } + + @Override + public void onStopped() { + } + + @Override + public void onError(UVCPlayerException e) { + } + }); + mPlayer.start(camera, parameter); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private synchronized void stopCamera() { + if (mPlayer != null) { + try { + mPlayer.stop(); + } catch (Exception e) { + // ignore. + } + mPlayer = null; + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcVideoEncoder.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcVideoEncoder.java new file mode 100644 index 0000000000..2994c5b3a2 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/recorder/uvc/UvcVideoEncoder.java @@ -0,0 +1,38 @@ +package org.deviceconnect.android.deviceplugin.uvc.recorder.uvc; + +import org.deviceconnect.android.libmedia.streaming.video.CameraVideoQuality; +import org.deviceconnect.android.libmedia.streaming.video.SurfaceVideoEncoder; +import org.deviceconnect.android.libmedia.streaming.video.VideoQuality; + +public class UvcVideoEncoder extends SurfaceVideoEncoder { + /** + * 映像のエンコード設定. + */ + private final CameraVideoQuality mVideoQuality; + + public UvcVideoEncoder(UvcRecorder recorder) { + this(recorder, "video/avc"); + } + + public UvcVideoEncoder(UvcRecorder recorder, String mimeType) { + super(recorder.getSurfaceDrawingThread()); + mVideoQuality = new CameraVideoQuality(mimeType); + } + + // VideoEncoder + + @Override + public VideoQuality getVideoQuality() { + return mVideoQuality; + } + + // SurfaceVideoEncoder + + @Override + protected void onStartSurfaceDrawing() { + } + + @Override + protected void onStopSurfaceDrawing() { + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/service/UVCService.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/service/UVCService.java index f29589483d..0a87459562 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/service/UVCService.java +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/service/UVCService.java @@ -1,43 +1,89 @@ package org.deviceconnect.android.deviceplugin.uvc.service; -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDevice; -import org.deviceconnect.android.deviceplugin.uvc.core.UVCDeviceManager; +import android.content.Context; +import android.util.Log; + import org.deviceconnect.android.deviceplugin.uvc.profile.UVCMediaStreamRecordingProfile; -import org.deviceconnect.android.deviceplugin.uvc.recorder.UVCRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorderManager; +import org.deviceconnect.android.deviceplugin.uvc.recorder.h264.UvcH264Recorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.mjpeg.UvcMjpgRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.uncompressed.UvcUncompressedRecorder; +import org.deviceconnect.android.deviceplugin.uvc.recorder.uvc.UvcRecorder; +import org.deviceconnect.android.libuvc.Parameter; +import org.deviceconnect.android.libuvc.UVCCamera; import org.deviceconnect.android.service.DConnectService; -import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; public class UVCService extends DConnectService { - private UVCDeviceManager mDeviceManager; - private UVCRecorder mUVCRecorder; - public UVCService(UVCDeviceManager deviceMgr, UVCDevice device) { - super(device.getId()); + private final Context mContext; + private MediaRecorderManager mMediaRecorderManager; + + public UVCService(Context context, String serviceId) { + super(serviceId); + mContext = context; - mDeviceManager = deviceMgr; - setName("UVC: " + device.getName()); setOnline(false); setNetworkType(NetworkType.UNKNOWN); + addProfile(new UVCMediaStreamRecordingProfile()); } - public UVCRecorder getUVCRecorder() { - return mUVCRecorder; + /** + * UVC に接続した時の処理を行います. + * + * @param camera UVC デバイス + */ + public synchronized void connect(UVCCamera camera) { + setName(camera.getDeviceName()); + setOnline(true); + + if (mMediaRecorderManager != null) { + mMediaRecorderManager.destroy(); + } + mMediaRecorderManager = new MediaRecorderManager(mContext, camera); } - public void openUVCDevice(UVCDevice device) { + /** + * UVC が切断された時の処理を行います. + */ + public synchronized void disconnect() { + setOnline(false); + if (mMediaRecorderManager != null) { + mMediaRecorderManager.destroy(); + mMediaRecorderManager = null; + } + } - mUVCRecorder = new UVCRecorder(mDeviceManager, device); - mUVCRecorder.initialize(); + public synchronized MediaRecorderManager getMediaRecorderManager() { + return mMediaRecorderManager; } - public void closeUVCDevice() { - mUVCRecorder.clean(); + /** + * レコーダのリストを取得します. + * + * @return レコーダ + */ + public List getUvcRecorderList() { + return mMediaRecorderManager != null ? mMediaRecorderManager.getUvcRecorderList() : new ArrayList<>(); } - public void reset() { - if (mUVCRecorder != null) { - mUVCRecorder.stopPreview(); - } + /** + * レコーダ ID を指定してレコーダを取得します. + * + * レコーダが見つからない場合は null を返却します。 + * + * @param id レコーダID + * @return レコーダ + */ + public UvcRecorder findUvcRecorderById(String id) { + return mMediaRecorderManager != null ? mMediaRecorderManager.findUvcRecorderById(id) : null; + } + + public UvcRecorder getDefaultRecorder() { + return mMediaRecorderManager != null ? mMediaRecorderManager.getDefaultRecorder() : null; } } diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/CapabilityUtil.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/CapabilityUtil.java new file mode 100644 index 0000000000..4cfc46072b --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/CapabilityUtil.java @@ -0,0 +1,201 @@ +/* + CapabilityUtil.java + Copyright (c) 2016 NTT DOCOMO,INC. + Released under the MIT license + http://opensource.org/licenses/mit-license.php + */ +package org.deviceconnect.android.deviceplugin.uvc.util; + +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.os.Build; +import android.util.Size; + +import org.deviceconnect.android.deviceplugin.uvc.recorder.MediaRecorder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class CapabilityUtil { + private CapabilityUtil() { + } + + private static List getMediaCodecInfoList() { + List infoList = new ArrayList<>(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS); + infoList.addAll(Arrays.asList(list.getCodecInfos())); + } else { + for (int j = MediaCodecList.getCodecCount() - 1; j >= 0; j--) { + infoList.add(MediaCodecList.getCodecInfoAt(j)); + } + } + return infoList; + } + + public static List getSupportedProfileLevel(String mimeType) { + List list = new ArrayList<>(); + + for (MediaCodecInfo codecInfo : getMediaCodecInfoList()) { + if (codecInfo.isEncoder()) { + String[] types = codecInfo.getSupportedTypes(); + if (Arrays.asList(types).contains(mimeType)) { + MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(mimeType); + if (codecCapabilities.profileLevels != null) { + for (MediaCodecInfo.CodecProfileLevel c : codecCapabilities.profileLevels) { + list.add(new MediaRecorder.ProfileLevel(c.profile, c.level)); + } + } + } + } + } + + return list; + } + + private static List getSupportedEncoders(String mimeType) { + List encoderList = new ArrayList<>(); + + for (MediaCodecInfo codecInfo : getMediaCodecInfoList()) { + if (codecInfo.isEncoder()) { + String[] types = codecInfo.getSupportedTypes(); + for (String type : types) { + if (!type.startsWith(mimeType)) { + continue; + } + encoderList.add(type); + } + } + } + + return encoderList; + } + + /** + * サポートされている音声コーデックのリストを取得します. + * + * @return サポートされている音声コーデックのリスト + */ + public static List getSupportedAudioEncoders() { + return getSupportedEncoders("audio/"); + } + + /** + * サポートされている映像コーデックのリストを取得します. + * + * @return サポートされている映像コーデックのリスト + */ + public static List getSupportedVideoEncoders() { + return getSupportedEncoders("video/"); + } + + /** + * MediaCodec でサポートされている解像度の最大値を取得します. + * + * @param mimeType マイムタイプ + * @return サポートされている解像度の最大値 + */ + public static Size getSupportedMaxSize(String mimeType) { + List sizeList = new ArrayList<>(); + + for (MediaCodecInfo codecInfo : getMediaCodecInfoList()) { + if (codecInfo.isEncoder() && + isHardware(codecInfo) && + isMediaCodecInfo(codecInfo, mimeType, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)) { + Size size = getSizeFromCodecInfo(codecInfo, mimeType); + if (size != null) { + sizeList.add(size); + } + } + } + + Size size = null; + int max = 0; + + for (Size s : sizeList) { + int d = s.getWidth() * s.getHeight(); + if (max < d) { + max = d; + size = s; + } + } + + return size; + } + + /** + * MediaCodecInfo から解像度を取得します. + * + * @param codecInfo コーデック情報 + * @param mimeType マイムタイプ + * @return 解像度 + */ + private static Size getSizeFromCodecInfo(MediaCodecInfo codecInfo, String mimeType) { + String[] types = codecInfo.getSupportedTypes(); + for (String type : types) { + if (!type.startsWith(mimeType)) { + continue; + } + + try { + MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(type); + MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); + if (videoCapabilities != null) { + int w = videoCapabilities.getSupportedWidths().getUpper(); + int h = videoCapabilities.getSupportedHeights().getUpper(); + return new Size(w, h); + } + } catch (Exception e) { + // ignore. + } + } + return null; + } + + /** + * 指定された MediaCodecInfo のマイムタイプとカラーフォーマットが一致するか確認します. + * + * @param codecInfo 確認する MediaCodecInfo + * @param mimeType マイムタイプ + * @param colorFormat カラーフォーマット + * @return 一致する場合はtrue、それ以外はfalse + */ + private static boolean isMediaCodecInfo(MediaCodecInfo codecInfo, String mimeType, int colorFormat) { + if (!codecInfo.isEncoder()) { + return false; + } + + String[] types = codecInfo.getSupportedTypes(); + for (String type : types) { + if (!type.equalsIgnoreCase(mimeType)) { + continue; + } + + try { + MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(type); + for (int i = 0; i < capabilities.colorFormats.length; i++) { + int format = capabilities.colorFormats[i]; + if (colorFormat == format) { + return true; + } + } + } catch (Exception e) { + // ignore. + } + } + + return false; + } + + private static boolean isHardware(MediaCodecInfo info) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return info.isHardwareAccelerated(); + } else { + // エンコーダ名が OMX.qcom. または OMX.Exynos. から始まる場合はハードウェアエンコーダ + // エンコーダ名が OMX.google. から始まる場合はソフトウェアエンコーダ + String name = info.getName(); + return name.startsWith("OMX.qcom.") || name.startsWith("OMX.Exynos."); + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/PropertyUtil.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/PropertyUtil.java new file mode 100644 index 0000000000..fc5073b9bc --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/PropertyUtil.java @@ -0,0 +1,144 @@ +package org.deviceconnect.android.deviceplugin.uvc.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Rect; +import android.util.Log; +import android.util.Size; + +import java.util.Set; + +/** + * Properties を使用して、データを保存するためのユーテリティクラス. + */ +public final class PropertyUtil { + private final SharedPreferences mPref; + + public PropertyUtil(Context context, String name) { + mPref = context.getSharedPreferences(name, Context.MODE_PRIVATE); + } + + public void clear() { + mPref.edit().clear().apply(); + } + + public Set getKeys() { + return mPref.getAll().keySet(); + } + + public void put(String key, int value) { + mPref.edit().putString(key, String.valueOf(value)).apply(); + } + + public void put(String key, long value) { + mPref.edit().putString(key, String.valueOf(value)).apply(); + } + + public void put(String key, float value) { + mPref.edit().putString(key, String.valueOf(value)).apply(); + } + + public void put(String key, boolean value) { + mPref.edit().putBoolean(key, value).apply(); + } + + public void put(String key, String value) { + mPref.edit().putString(key, value).apply(); + } + + public void put(String widthKey, String heightKey, Size size) { + mPref.edit().putString(widthKey, String.valueOf(size.getWidth())) + .putString(heightKey, String.valueOf(size.getHeight())) + .apply(); + } + + public void put(String leftKey, String topKey, String rightKey, String bottomKey, Rect rect) { + mPref.edit().putString(leftKey, String.valueOf(rect.left)) + .putString(topKey, String.valueOf(rect.top)) + .putString(rightKey, String.valueOf(rect.right)) + .putString(bottomKey, String.valueOf(rect.bottom)) + .apply(); + } + + public Integer getInteger(String key, Integer defaultValue) { + String value = mPref.getString(key, String.valueOf(defaultValue)); + try { + if (value != null) { + return Integer.parseInt(value); + } + } catch (Exception e) { + // ignore. + } + return defaultValue; + } + + public Long getLong(String key, Long defaultValue) { + String value = mPref.getString(key, String.valueOf(defaultValue)); + try { + if (value != null) { + return Long.parseLong(value); + } + } catch (Exception e) { + // ignore. + } + return defaultValue; + } + + public String getString(String key, String defaultValue) { + return mPref.getString(key, defaultValue); + } + + public Float getFloat(String key, Float defaultValue) { + String value = mPref.getString(key, String.valueOf(defaultValue)); + try { + if (value != null) { + return Float.parseFloat(value); + } + } catch (Exception e) { + // ignore. + } + return defaultValue; + } + + public boolean getBoolean(String key, boolean defaultValue) { + return mPref.getBoolean(key, defaultValue); + } + + public Size getSize(String widthKey, String heightKey) { + String w = mPref.getString(widthKey, null); + String h = mPref.getString(heightKey, null); + if (w != null && h != null) { + try { + return new Size(Integer.parseInt(w), Integer.parseInt(h)); + } catch (Exception e) { + // ignore. + } + } + return null; + } + + public Rect getRect(String leftKey, String topKey, String rightKey, String bottomKey) { + String l = mPref.getString(leftKey, null); + String t = mPref.getString(topKey, null); + String r = mPref.getString(rightKey, null); + String b = mPref.getString(bottomKey, null); + if (l != null && t != null && r != null && b != null) { + try { + int left = Integer.parseInt(l); + int top = Integer.parseInt(t); + int right = Integer.parseInt(r); + int bottom = Integer.parseInt(b); + if (left >= 0 && top >= 0 && right >= 0 && bottom >= 0) { + return new Rect(left, top, right, bottom); + } + } catch (Exception e) { + // ignore. + } + } + return null; + } + + public void remove(String key) { + mPref.edit().remove(key).apply(); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/SRTSettings.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/SRTSettings.java new file mode 100644 index 0000000000..177e0968e4 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/SRTSettings.java @@ -0,0 +1,101 @@ +package org.deviceconnect.android.deviceplugin.uvc.util; + +import android.content.Context; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SRTSettings { + + public static final String FILE_NAME = "srt_properties"; + + /** + * 設定画面でサポートする SRT オプションの定義. + */ + private final List mSRTOptionItems = new ArrayList<>(); + { +// mSRTOptionItems.add(new SRTOptionItem(SRT.SRTO_PEERLATENCY, Integer.class, R.string.pref_key_settings_srt_peerlatency)); +// mSRTOptionItems.add(new SRTOptionItem(SRT.SRTO_LOSSMAXTTL, Integer.class, R.string.pref_key_settings_srt_lossmaxttl)); +// mSRTOptionItems.add(new SRTOptionItem(SRT.SRTO_INPUTBW, Long.class, R.string.pref_key_settings_srt_inputbw)); +// mSRTOptionItems.add(new SRTOptionItem(SRT.SRTO_OHEADBW, Integer.class, R.string.pref_key_settings_srt_oheadbw)); +// mSRTOptionItems.add(new SRTOptionItem(SRT.SRTO_CONNTIMEO, Integer.class, R.string.pref_key_settings_srt_conntimeo)); +// mSRTOptionItems.add(new SRTOptionItem(SRT.SRTO_PEERIDLETIMEO, Integer.class, R.string.pref_key_settings_srt_peeridletimeo)); +// mSRTOptionItems.add(new SRTOptionItem(SRT.SRTO_PACKETFILTER, String.class, R.string.pref_key_settings_srt_packetfilter)); + } + + private final PropertyUtil mPref; + private final Context mContext; + + public SRTSettings(Context context) { + mContext = context; + mPref = new PropertyUtil(context, FILE_NAME); + } + + /** + * 指定されたキーの値を整数にして取得します. + * + * @param key 格納されているキー + * @param defaultValue 値が格納されていない場合に返却する値 + * @return 整数値 + */ + private int getInt(String key, int defaultValue) { + String value = mPref.getString(key, String.valueOf(defaultValue)); + try { + return Integer.parseInt(value); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * SRT サーバに対して設定するオプションの一覧を作成します. + * + * @return オプションの一覧 + */ + public Map loadSRTSocketOptions() { + Map options = new HashMap<>(); + for (SRTOptionItem item : mSRTOptionItems) { + options.put(item.getOptionEnum(), item.getValue()); + } + return options; + } + + /** + * SRT オプション設定項目の定義. + * + * SRT オプションの列挙子 ({@link SRT} で定義されているもの) に対して、値の型とプリファレンスキーを対応づける. + */ + private class SRTOptionItem { + final int mOptionEnum; + final Class mValueClass; + final int mPrefKey; + + SRTOptionItem(int optionEnum, Class valueClass, int prefKey) { + mOptionEnum = optionEnum; + mValueClass = valueClass; + mPrefKey = prefKey; + } + + int getOptionEnum() { + return mOptionEnum; + } + + Object getValue() { + String key = mContext.getString(mPrefKey); + String value = mPref.getString(key, null); + if (value == null || "".equals(value)) { + return null; + } + try { + if (mValueClass == Long.class) { + return Long.parseLong(value); + } else if (mValueClass == Integer.class) { + return Integer.parseInt(value); + } + } catch (Exception ignored) {} + return value; + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/UVCRegistry.java b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/UVCRegistry.java new file mode 100644 index 0000000000..65d2100f78 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/java/org/deviceconnect/android/deviceplugin/uvc/util/UVCRegistry.java @@ -0,0 +1,142 @@ +package org.deviceconnect.android.deviceplugin.uvc.util; + +import android.content.Context; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class UVCRegistry { + private static final String FILE_NAME = "uvc-save.dat"; + + private final List mUVCList = new ArrayList<>(); + private final Context mContext; + + public UVCRegistry(Context context) { + mContext = context; + load(); + } + + public List getUVCList() { + return mUVCList; + } + + public void addUVC(String deviceId, String name) { + UVC uvc = new UVC(deviceId, name); + if (!mUVCList.contains(uvc)) { + mUVCList.add(uvc); + } + save(); + } + + public void removeUVC(String deviceId) { + UVC removeUVC = null; + for (UVC uvc : mUVCList) { + if (uvc.getDeviceId().equalsIgnoreCase(deviceId)) { + removeUVC = uvc; + break; + } + } + if (removeUVC != null) { + mUVCList.remove(removeUVC); + } + save(); + } + + private void removeUVC(UVC uvc) { + mUVCList.remove(uvc); + } + + private void save() { + JSONArray array = new JSONArray(); + for (UVC uvc : mUVCList) { + try { + array.put(createObject(uvc)); + } catch (Exception e) { + // ignore. + } + } + + String text = array.toString(); + try (FileOutputStream fos = mContext.openFileOutput(FILE_NAME, Context.MODE_PRIVATE)) { + fos.write(text.getBytes()); + } catch (Exception e) { + // ignore. + } + } + + private void load() { + int len; + byte[] buf = new byte[4092]; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (FileInputStream ios = mContext.openFileInput(FILE_NAME)) { + while ((len = ios.read(buf)) > 0) { + baos.write(buf, 0, len); + } + } catch (Exception e) { + // ignore. + } + + mUVCList.clear(); + try { + JSONArray array = new JSONArray(new String(baos.toByteArray())); + for (int i = 0; i < array.length(); i++) { + mUVCList.add(createUVC(array.getJSONObject(i))); + } + } catch (Exception e) { + // ignore. + } + } + + private JSONObject createObject(UVC uvc) throws JSONException { + JSONObject object = new JSONObject(); + object.put("deviceId", uvc.getDeviceId()); + object.put("name", uvc.getName()); + return object; + } + + private UVC createUVC(JSONObject object) throws JSONException { + String deviceId = object.getString("deviceId"); + String name = object.getString("name"); + return new UVC(deviceId, name); + } + + public static class UVC { + private final String mDeviceId; + private final String mName; + + public UVC(String deviceId, String name) { + mDeviceId = deviceId; + mName = name; + } + + public String getDeviceId() { + return mDeviceId; + } + + public String getName() { + return mName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UVC uvc = (UVC) o; + return Objects.equals(mDeviceId, uvc.mDeviceId) && + Objects.equals(mName, uvc.mName); + } + + @Override + public int hashCode() { + return Objects.hash(mDeviceId, mName); + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/drawable-hdpi/dconnect_icon_lollipop.png b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/drawable-hdpi/dconnect_icon_lollipop.png new file mode 100644 index 0000000000..c20d88d29c Binary files /dev/null and b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/drawable-hdpi/dconnect_icon_lollipop.png differ diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/drawable-xhdpi/button_red.xml b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/drawable/button_red.xml similarity index 100% rename from dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/drawable-xhdpi/button_red.xml rename to dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/drawable/button_red.xml diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/activity_uvc_settings.xml b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/activity_uvc_settings.xml new file mode 100644 index 0000000000..75c02043e3 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/activity_uvc_settings.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/dialog_preference_seek_bar.xml b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/dialog_preference_seek_bar.xml new file mode 100644 index 0000000000..eb4be8c649 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/dialog_preference_seek_bar.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/dialog_progress.xml b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/dialog_progress.xml deleted file mode 100644 index f9217db3d6..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/dialog_progress.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/fragment_uvc_device_instruction.xml b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/fragment_uvc_device_instruction.xml index 934a972e97..8654d01a7c 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/fragment_uvc_device_instruction.xml +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/fragment_uvc_device_instruction.xml @@ -1,33 +1,14 @@ + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="16dp"> - - - - - - - - + android:paddingTop="10dp" + android:text="@string/uvc_settings_instruction_uvc_device_connection" /> \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/fragment_uvc_device_list.xml b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/fragment_uvc_device_list.xml index bd4353b404..8a53e95770 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/fragment_uvc_device_list.xml +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/fragment_uvc_device_list.xml @@ -1,30 +1,27 @@ - + + - + - + + - + - - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:onItemClickListener="@{presenter.onItemClick}" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/fragment_uvc_permission_confirmation.xml b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/fragment_uvc_permission_confirmation.xml new file mode 100644 index 0000000000..68726662a7 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/src/main/res/layout/fragment_uvc_permission_confirmation.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + +