diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/build.gradle b/dConnectDevicePlugin/dConnectDeviceHost/app/build.gradle index 521ff4afb1..ad665e41e6 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/build.gradle +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/build.gradle @@ -33,7 +33,7 @@ android { buildConfigField "String", "DEMO_DIR", "\"demo\"" buildConfigField "String", "DEMO_ZIP", "\"" + DEMO_ZIP_NAME + "\"" buildConfigField "long", "STATS_INTERVAL", "5000L" - } + } signingConfigs { releaseConfig { @@ -103,8 +103,8 @@ dependencies { 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:dconnect-demo-lib:1.0.1' - implementation 'org.deviceconnect:libmedia:1.2.2' - implementation 'org.deviceconnect:libsrt:1.2.2' + implementation 'org.deviceconnect:libmedia:1.3.0' + implementation 'org.deviceconnect:libsrt:1.3.0' // implementation project(':libmedia') // implementation project(':libsrt') } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/AndroidManifest.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/AndroidManifest.xml index 0a82c67962..82ac7e2549 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/AndroidManifest.xml +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/AndroidManifest.xml @@ -112,6 +112,7 @@ android:name="org.deviceconnect.android.deviceplugin.host.setting.HostSettingActivity" android:exported="false" android:launchMode="singleTask" + android:taskAffinity=".settings" android:theme="@style/AppCompatTheme" /> @@ -119,6 +120,7 @@ android:name=".activity.recorder.settings.SettingsActivity" android:configChanges="keyboardHidden|orientation|screenSize" android:exported="false" + android:taskAffinity=".recorder_settings" android:theme="@style/AppCompatTheme" /> diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/assets/org_deviceconnect_android_deviceplugin_host/api/mediaStreamRecording.json b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/assets/org_deviceconnect_android_deviceplugin_host/api/mediaStreamRecording.json index 9ef4f73879..5d85065860 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/assets/org_deviceconnect_android_deviceplugin_host/api/mediaStreamRecording.json +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/assets/org_deviceconnect_android_deviceplugin_host/api/mediaStreamRecording.json @@ -1,1903 +1,1931 @@ + { - "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" - } - } - } - } - } + "swagger": "2.0", + "info": { + "description": "スマートデバイスによる写真撮影、動画録画または音声録音などの操作を行うAPI。", + "version": "2.0.0", + "title": "Media Stream Recording Profile" }, - "/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" + "basePath": "/gotapi/mediaStreamRecording", + "consumes": ["application/x-www-form-urlencoded", "multipart/form-data"], + "paths": { + "/crop": { + "put": { + "summary": "配信を行う映像の範囲を指定します。", + "description": "現在の配信範囲から指定された範囲まで移動します。", + "operationId": "mediaStreamRecordingCropPut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + }, + { + "name": "name", + "in": "formData", + "description": "範囲指定を行うサーバの名前。 省略された場合は、全てのサーバに範囲指定を行います。", + "required": false, + "type": "string" + }, + { + "name": "left", + "in": "formData", + "description": "範囲の左座標を指定します。", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "top", + "in": "formData", + "description": "範囲の上座標を指定します。", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "right", + "in": "formData", + "description": "範囲の右座標を指定します。", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "bottom", + "in": "formData", + "description": "範囲の下座標を指定します。", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "duration", + "in": "formData", + "description": "指定された時間(ミリ秒)をかけて、指定された座標に移動します。 省略された場合には、瞬時に移動します。", + "required": false, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "プレビュー配信を行う範囲を設定結果を返す。", + "schema": { + "$ref": "#/definitions/CommonResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" }, - "examples": { - "application/json": { - "result": 0, - "product": "Example System", - "version": "1.0.0" - } + "delete": { + "summary": "プレビュー配信を行う範囲を解除します。", + "description": "プレビュー配信を行う範囲を解除します。", + "operationId": "mediaStreamRecordingCropDelete", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "query", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + }, + { + "name": "name", + "in": "query", + "description": "解除するサーバの名前。 省略された場合は、全ての範囲指定を解除します。", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "プレビュー配信を行う範囲を解除結果を返す。 解除に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/CommonResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" } - } - } - } - }, - "/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" + }, + "/encoder": { + "put": { + "summary": "配信を行うエンコーダの設定を行います。", + "description": "配信を行うエンコーダの設定を行います。", + "operationId": "mediaStreamRecordingEncoderPut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + }, + { + "name": "name", + "in": "formData", + "description": "エンコーダの設定を行うサーバの名前。 省略された場合は、全てのエンコーダに対して処理を行います。", + "required": false, + "type": "string" + }, + { + "name": "mimeType", + "in": "formData", + "description": "マイムタイプを指定します。
以下のマイムタイプをサポートします。
- video/x-mjpeg
- video/x-rtp
- video/MP2T
- video/x-rtmp", + "required": false, + "type": "string" + }, + { + "name": "width", + "in": "formData", + "description": "配信する映像の横幅を指定します。", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "height", + "in": "formData", + "description": "配信する映像の縦幅を指定します。", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "frameRate", + "in": "formData", + "description": "配信する映像のフレームレートを指定します。", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "bitRate", + "in": "formData", + "description": "配信する映像のビットレートを指定します。", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "keyFrameInterval", + "in": "formData", + "description": "配信する映像のキーフレームインターバルを指定します。", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "codec", + "in": "formData", + "description": "配信する映像のコーデックを指定します。
サポートするコーデック
- h264
- h265", + "required": false, + "type": "string" + }, + { + "name": "profile", + "in": "formData", + "description": "配信する映像のコーデックのプロファイルを指定します。
サポートされるプロファイルは端末ごとに異なります。", + "required": false, + "type": "string" + }, + { + "name": "level", + "in": "formData", + "description": "配信する映像のコーデックのレベルを指定します。
サポートされるレベルは端末ごとに異なります。", + "required": false, + "type": "string" + }, + { + "name": "useSoftwareEncoder", + "in": "formData", + "description": "配信する映像のエンコーダにソフトウェアエンコーダを指定します。", + "required": false, + "type": "boolean" + }, + { + "name": "intraRefresh", + "in": "formData", + "description": "配信する映像のイントラリフレッシュを指定します。", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "jpegQuality", + "in": "formData", + "description": "配信する映像のJPEGの品質を指定します。
video/x-mjpeg のみサポートします。", + "required": false, + "type": "number", + "maximum": 1.0, + "minimum": 0.0 + }, + { + "name": "broadcastUri", + "in": "formData", + "description": "配信先の URI を指定します。
video/x-rtmp のみサポートします。", + "required": false, + "type": "string" + }, + { + "name": "retryCount", + "in": "formData", + "description": "配信先の URI への接続リトライ回数を指定します。
video/x-rtmp のみサポートします。", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "retryInterval", + "in": "formData", + "description": "配信先の URI への接続リトライの間隔を指定します。
video/x-rtmp のみサポートします。", + "required": false, + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "配信するエンコーダへのパラメータ設定処理の結果を返す。 設定に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/CommonResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" }, - "examples": { - "application/json": { - "result": 0, - "product": "Example System", - "version": "1.0.0" - } + "delete": { + "summary": "配信を行うエンコーダの削除を行います。", + "description": "配信を行うエンコーダの削除を行います。", + "operationId": "mediaStreamRecordingEncoderDelete", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "query", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + }, + { + "name": "name", + "in": "query", + "description": "削除するエンコーダの名前。", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "指定されたエンコーダの削除結果を返す。 削除に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/CommonResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" } - } - } - } - }, - "/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" - } + }, + "/mediaRecorder": { + "get": { + "summary": "スマートデバイスから使用可能なレコーダーの一覧を取得する。", + "description": "ストリーミング配信する機能をレコーダーとして扱うことができる。 例えば、スマートフォンのカメラの映像や スマートフォンのデスクトップのスクリーンキャストなどをレコーダとして扱ったりすることができる。", + "operationId": "mediaStreamRecordingMediaRecorderGet", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "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": "" + } + ] + } + } + } + }, + "x-type": "one-shot" } - } - } - } - }, - "/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" - } + }, + "/muteTrack": { + "put": { + "summary": "スマートデバイスに対して動画撮影や音声録音のミュートリクエストを送る。", + "description": "ビデオとオーディオなどの複数トラックが含まれる撮影中メディアにおいては、 オーディオトラックがミュートされる。", + "operationId": "mediaStreamRecordingMuteTrackPut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "ミュートリクエストを送った結果を返す。 ミュートリクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" } - } - } - } - }, - "/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" + }, + "/onPhoto": { + "get": { + "summary": "スマートデバイスの写真撮影通知イベントを取得する。", + "description": "プラグイン側でキャッシュしている最新のイベントメッセージを1つ取得する。", + "operationId": "mediaStreamRecordingOnPhotoGet", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "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" + } + } + } + }, + "x-type": "one-shot" }, - "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 - } + "put": { + "summary": "スマートデバイスの写真撮影通知イベントを開始する。", + "description": "スマートデバイスでサポートしていないパラメータがある場合には、 そのパラメータを省略する。", + "operationId": "mediaStreamRecordingOnPhotoPut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "interval", + "in": "formData", + "description": "デバイスプラグインがイベントを送信する間隔。", + "required": false, + "type": "integer", + "format": "int64" + } ], - "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 - } + "responses": { + "200": { + "description": "通知イベントの開始結果を返す。 開始に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/EventRegistrationResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "event", + "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": { + "summary": "スマートデバイスの写真撮影通知イベントを停止する。", + "description": "スマートデバイスでサポートしていないパラメータがある場合には、 そのパラメータを省略する。", + "operationId": "mediaStreamRecordingOnPhotoDelete", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + } ], - "mimeType": [ - "image/png" - ] - } + "responses": { + "200": { + "description": "通知イベントの停止結果を返す。 停止に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/EventUnregistrationResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "event" } - } - } - }, - "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" + }, + "/onRecordingChange": { + "get": { + "summary": "スマートデバイスでの写真撮影、動画撮影または音声録音の状態変化通知イベントを 取得する。", + "description": "プラグイン側でキャッシュしている最新のイベントメッセージを1つ取得する。", + "operationId": "mediaStreamRecordingOnRecordingChangeGet", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "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" + } + } + } + } + }, + "x-type": "one-shot" }, - "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" + "put": { + "summary": "スマートデバイスでの写真撮影、動画撮影または音声録音の状態変化通知イベントを 開始する。", + "description": "スマートデバイスでサポートしていないパラメータがある場合には、 そのパラメータを省略する。", + "operationId": "mediaStreamRecordingOnRecordingChangePut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "interval", + "in": "formData", + "description": "デバイスプラグインがイベントを送信する間隔。", + "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-type": "event", + "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" + } + } + } + } }, - "examples": { - "application/json": { - "result": 0, - "product": "Example System", - "version": "1.0.0", - "uri": "Example URI", - "path": "Example Path" - } + "delete": { + "summary": "スマートデバイスでの写真撮影、動画撮影または音声録音の状態変化通知イベントを 停止する。", + "description": "スマートデバイスでサポートしていないパラメータがある場合には、 そのパラメータを省略する。", + "operationId": "mediaStreamRecordingOnRecordingChangeDelete", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "撮影または録音の状態変化通知イベントの停止結果を返す。 停止に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/EventUnregistrationResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "event" } - } - } - }, - "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" + }, + "/options": { + "get": { + "summary": "スマートデバイスからサポートしている写真撮影、 動画撮影や音声録音のオプションを取得する。", + "description": "ターゲットがサポートしている解像度などの一覧を返す。", + "operationId": "mediaStreamRecordingOptionsGet", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "query", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "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"] + } + } + } + }, + "x-type": "one-shot" }, - "examples": { - "application/json": { - "result": 0, - "product": "Example System", - "version": "1.0.0" - } + "put": { + "summary": "スマートデバイスからサポートしている写真撮影、 動画撮影や音声録音のオプションを設定する。", + "description": "スマートデバイスのPreviewSize,PictureSizeや最大フレームレートなどを設定する。", + "operationId": "mediaStreamRecordingOptionsPut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + }, + { + "name": "imageWidth", + "in": "formData", + "description": "撮影時の横幅。単位はピクセル。previewHeightを指定するときは省略不可。Options API GETで返された値以外を指定した場合はパラメータエラー。", + "required": false, + "type": "integer" + }, + { + "name": "imageHeight", + "in": "formData", + "description": "撮影時の縦幅。単位はピクセル。previewWidthを指定するときは省略不可。
Options API GETで返された値以外を指定した場合はパラメータエラー。", + "required": false, + "type": "integer" + }, + { + "name": "previewWidth", + "in": "formData", + "description": "プレビュー時の横幅。単位はピクセル。previewHeightを指定するときは省略不可。
Options API GETで返された値以外を指定した場合はパラメータエラー。", + "required": false, + "type": "integer" + }, + { + "name": "previewHeight", + "in": "formData", + "description": "プレビュー時の縦幅。単位はピクセル。previewWidthを指定するときは省略不可。
Options API GETで返された値以外を指定した場合はパラメータエラー。", + "required": false, + "type": "integer" + }, + { + "name": "previewMaxFrameRate", + "in": "formData", + "description": "プレビュー時の最大フレームレート。単位はfps。範囲は0.0より大きい小数値。", + "required": false, + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + { + "name": "previewBitRate", + "in": "formData", + "description": "プレビューのビットレート。単位はKbps。", + "required": false, + "type": "integer" + }, + { + "name": "previewJpegQuality", + "in": "formData", + "description": "JPEGの品質。範囲は0.0から1.0。MotionJPEGのみサポート。", + "required": false, + "type": "number", + "minimum": 0, + "exclusiveMinimum": false, + "maximum": 1, + "exclusiveMaximum": false + }, + { + "name": "mimeType", + "in": "formData", + "description": "MimeType。動画録画・音声録音するときのエンコードするタイプ。", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "オプションの設定結果を返す。 設定に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" } - } }, - "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" - } + "/pause": { + "put": { + "summary": "スマートデバイスに対して動画撮影または音声録音の一時停止リクエストを送る。", + "description": "すでに撮影または録音が一時停止になっている場合や撮影または録音が行われていない場合は、 エラーを返す。", + "operationId": "mediaStreamRecordingPausePut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "動画撮影または音声録音の一時停止送信結果を返す。 一時停止送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" } - } - } - } - }, - "/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" + }, + "/preview": { + "get": { + "summary": "スマートデバイスでの動画撮影中に、動画のプレビュー画像を取得する。", + "description": "このイベント通知を行うタイミングに規定は無く、 デバイスプラグインの実装依存とする。
JPEGでデータを受信する。", + "operationId": "mediaStreamRecordingPreviewGet", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "query", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "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" + } + } + } + }, + "x-type": "one-shot" }, - "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" + "put": { + "summary": "スマートデバイスでの動画撮影中に、動画のプレビュー画像を受信を開始する。", + "description": "このイベント通知を行うタイミングに規定は無く、 デバイスプラグインの実装依存とする。MotionJPEGでデータを受信する。", + "operationId": "mediaStreamRecordingPreviewPut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "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" + } + } + } + }, + "x-type": "streaming" }, - "examples": { - "application/json": { - "streams": [ - { - "mimeType": "video/x-mjpeg", - "uri": "http://localhost:9000/xxxxxx" - }, - { - "mimeType": "video/x-rtp", - "uri": "rtsp://localhost:8086" - } + "delete": { + "summary": "スマートデバイスでの動画撮影中に、動画のプレビュー画像を受信を停止する。", + "description": "このイベント通知を行うタイミングに規定は無く、 デバイスプラグインの実装依存とする。
MotionJPEGなどでデータを受信する。", + "operationId": "mediaStreamRecordingPreviewDelete", + "parameters": [ + { + "name": "serviceId", + "in": "query", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "query", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + } ], - "result": 0, - "product": "Example System", - "version": "1.0.0", - "uri": "Example Preview URI" - } + "responses": { + "200": { + "description": "ストリーミング映像のURIを返す。 映像のURIを返せない場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/PreviewStopResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "streaming" } - } - } - }, - "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" + }, + "/preview/mute": { + "put": { + "summary": "音声が付加されているPreview映像に対し、 その映像の音声をミュート状態にするリクエストを送る。", + "description": "例えば、Preview映像がRTSPの場合に、このAPIにより音声をミュート状態にする。", + "operationId": "mediaStreamRecordingPreviewMutePut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Previewの音声をミュート状態にするリクエストを送った結果を返す。 ミュートリクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" }, - "examples": { - "application/json": { - "result": 0, - "product": "Example System", - "version": "1.0.0" - } + "delete": { + "summary": "音声が付加されているPreview映像に対し、 その映像の音声のミュート状態を解除するリクエストを送る。", + "description": "例えば、Preview映像がRTSPの場合に、このAPIにより音声のミュート状態を解除する。", + "operationId": "mediaStreamRecordingPreviewMuteDelete", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Previewの音声のミュート状態を解除するリクエストを送った結果を返す。 ミュート解除リクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" } - } - } - } - }, - "/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" - } + }, + "/record": { + "post": { + "summary": "スマートデバイスに対して、動画撮影や音声録音の開始リクエストを送る。", + "description": "MediaStreamRecording Stopされない場合は各デバイスが撮影できる最大時間まで 撮影を行い、 その後撮影を停止する。", + "operationId": "mediaStreamRecordingRecordPost", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + }, + { + "name": "timeslice", + "in": "formData", + "description": "タイムスライス。
動画・音声入力などから得られた1フレームを新たに出力先メディアに 書き出すまでの待ち時間。単位はミリ秒。
1000/timesliceが出力メディアの固定フレームレート(フレーム/秒) に相当する。省略された場合には、デバイス毎の挙動でフレーム書き出しを行う。", + "required": false, + "type": "integer", + "minimum": 0, + "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" + } + } + } + }, + "x-type": "one-shot" } - } - } - }, - "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" - } + }, + "/resume": { + "put": { + "summary": "スマートデバイスに対して一時停止状態にある動画撮影または音声録音の 再開リクエストを送る。", + "description": "すでに撮影または録音状態になっている場合はエラーを返す。", + "operationId": "mediaStreamRecordingResumePut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "再開リクエストを送った結果を返す。 リクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" } - } - } - } - }, - "/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" - } + }, + "/stop": { + "put": { + "summary": "スマートデバイスに対して動画撮影または音声録音の終了リクエストを送る。", + "description": "すでに撮影または録音が行われていない場合はエラーを返す。", + "operationId": "mediaStreamRecordingStopPut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "終了リクエストを送った結果を返す。 終了リクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderStopResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" } - } - } - } - }, - "/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": "broadcastURI", - "description": "配信先のサーバURI。
rtmp://、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" - } + }, + "/takePhoto": { + "post": { + "summary": "スマートデバイスに対して写真撮影リクエストを送る。", + "description": "targetが指定されていない場合は、GET/mediaStreamRecording/mediaRecorderで 一番最初に見つかるレコーダーが指定される。", + "operationId": "mediaStreamRecordingTakePhotoPost", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "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" + } + } + } + }, + "x-type": "one-shot" } - } - } - }, - "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" - } + }, + "/unmuteTrack": { + "put": { + "summary": "スマートデバイスに対して動画撮影や音声録音のミュート解除リクエストを送る。", + "description": "ビデオとオーディオなどの複数トラックが含まれる撮影中メディアにおいては、 オーディオトラックがミュート解除される。", + "operationId": "mediaStreamRecordingUnmuteTrackPut", + "parameters": [ + { + "name": "serviceId", + "in": "formData", + "description": "サービスID。取得対象スマートデバイス", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "ターゲット。レコーダーを識別するID。
省略された場合にはデフォルトのレコーダーを使用する。
デバイスが音声・動画の両方のレコーダーをサポートする場合、 どちらのレコーダーが使用されるかどうかはデバイスプラグイン依存とする。", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "ミュート解除リクエストを送った結果を返す。 ミュート解除リクエストの送信に失敗した場合はエラーを返す。", + "schema": { + "$ref": "#/definitions/RecorderControlResponse" + }, + "examples": { + "application/json": { + "result": 0, + "product": "Example System", + "version": "1.0.0" + } + } + } + }, + "x-type": "one-shot" } - } } - } }, - "/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" + "definitions": { + "MediaRecorderResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "required": ["recorders"], + "properties": { + "recorders": { + "type": "array", + "description": "レコーダー情報の配列。", + "title": "レコーダーリスト", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "レコーダーを識別するID。", + "title": "レコーダーID" + }, + "name": { + "type": "string", + "description": "レコーダー名。", + "title": "レコーダー名" + }, + "state": { + "type": "string", + "description": "レコーダーの状態。", + "title": "レコーダーの状態", + "enum": ["inactive", "recording", "paused"] + }, + "mimeType": { + "type": "string", + "description": "レコーダーのエンコードするタイプ。", + "title": "MIME Type" + }, + "config": { + "type": "string", + "description": "カメラに設定がある場合には、ここに文字列としてデータが格納されている。", + "title": "コンフィグ" + }, + "imageWidth": { + "type": "integer", + "description": "レコーダーの現在の横幅。単位はピクセル。録音の場合は省略可。", + "title": "画像の横幅" + }, + "imageHeight": { + "type": "integer", + "description": "レコーダーの現在の縦幅。単位はピクセル。録音の場合は省略可。", + "title": "画像の縦幅" + }, + "previewWidth": { + "type": "integer", + "description": "プレビューの現在の横幅。単位はピクセル。録音の場合は省略可。", + "title": "プレビューの横幅" + }, + "previewHeight": { + "type": "integer", + "description": "プレビューの現在の縦幅。単位はピクセル。録音の場合は省略可。", + "title": "プレビューの縦幅" + }, + "previewMaxFrameRate": { + "type": "number", + "description": "現在のプレビューのフレームレートの最大値。単位はfps。
録音の場合は省略可。", + "title": "プレビューの最大フレームレート" + }, + "audio": { + "type": "object", + "description": "録音する音声に関する情報。", + "title": "音声情報", + "properties": { + "channels": { + "type": "integer", + "description": "音声のチャンネル数。", + "title": "チャンネル数" + }, + "sampleRate": { + "type": "number", + "description": "音声のサンプルレート。単位はHz。", + "title": "サンプルレート" + }, + "sampleSize": { + "type": "integer", + "description": "音声のサンプルサイズ。単位はビット。", + "title": "サンプルサイズ" + }, + "blockSize": { + "type": "integer", + "description": "音声のブロックサイズ。単位はバイト。", + "title": "ブロックサイズ" + } + }, + "required": ["blockSize", "channels", "sampleRate", "sampleSize"] + } + }, + "required": ["config", "id", "mimeType", "name", "state"] + } + } + } } - } - } - } - } - }, - "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": { + "TakePhotoResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { "type": "object", - "title": "音声情報", - "description": "録音する音声に関する情報。", - "required": [ - "channels", - "sampleRate", - "sampleSize", - "blockSize" - ], + "required": ["path", "uri"], "properties": { - "channels": { - "type": "integer", - "title": "チャンネル数", - "description": "音声のチャンネル数。" - }, - "sampleRate": { - "type": "number", - "title": "サンプルレート", - "description": "音声のサンプルレート。単位はHz。" - }, - "sampleSize": { - "type": "integer", - "title": "サンプルサイズ", - "description": "音声のサンプルサイズ。単位はビット。" - }, - "blockSize": { - "type": "integer", - "title": "ブロックサイズ", - "description": "音声のブロックサイズ。単位はバイト。" - } + "uri": { + "type": "string", + "description": "撮影された写真のURI。", + "title": "URI" + }, + "path": { + "type": "string", + "description": "撮影された写真へのファイルパス。ルートはデバイスプラグインごとに異なる。
File APIのパラメータとして使用可能。", + "title": "ファイルパス" + } } - } } - } - } - } - } - ] - }, - "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" + "RecordResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "動画または音声のURI。", + "title": "URI" + }, + "path": { + "type": "string", + "description": "動画または音声へのファイルパス。ルートはデバイスプラグインごとに異なる。
File APIのパラメータとして使用可能。", + "title": "ファイルパス" + } + } + } + ] }, - { - "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": "画像の縦幅。単位はピクセル。" - } + "OptionsResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "required": ["mimeType"], + "properties": { + "mimeType": { + "type": "array", + "description": "録画・録音する際のエンコードするタイプの一覧", + "title": "MIME Type", + "items": { + "type": "string" + } + }, + "imageSizes": { + "type": "array", + "description": "レコーダーのサポートする画像の解像度の一覧。録音の場合は省略可。
プラグイン側でリサイズすることでサポートするサイズも含めてよい。", + "title": "撮影時の解像度の一覧", + "items": { + "type": "object", + "description": "画像の解像度。", + "title": "解像度", + "properties": { + "width": { + "type": "integer", + "description": "画像の横幅。単位はピクセル。", + "title": "横幅" + }, + "height": { + "type": "integer", + "description": "画像の縦幅。単位はピクセル。", + "title": "縦幅" + } + }, + "required": ["height", "width"] + } + }, + "previewSizes": { + "type": "array", + "description": "プレビューで利用可能な解像度の一覧。
録音の場合、またはプレビューを提供しない場合は省略可。
プラグイン側でリサイズすることでサポートするサイズも含めてよい。", + "title": "プレビュー時の解像度の一覧", + "items": { + "type": "object", + "description": "画像の解像度。", + "title": "解像度", + "properties": { + "width": { + "type": "integer", + "description": "画像の横幅。単位はピクセル。", + "title": "横幅" + }, + "height": { + "type": "integer", + "description": "画像の縦幅。単位はピクセル。", + "title": "縦幅" + } + }, + "required": ["height", "width"] + } + } + } } - } - }, - "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": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "$ref": "#/definitions/PhotoInfo" } - } - } - } - } - ] - }, - "PhotoResponse": { - "type": "object", - "allOf": [ - { - "$ref": "#/definitions/CommonResponse" + ] }, - { - "$ref": "#/definitions/PhotoInfo" - } - ] - }, - "PhotoEvent": { - "type": "object", - "allOf": [ - { - "$ref": "#/definitions/CommonEvent" + "PhotoEvent": { + "allOf": [ + { + "$ref": "#/definitions/CommonEvent" + }, + { + "$ref": "#/definitions/PhotoInfo" + } + ] }, - { - "$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。動画・音声を識別するために使用する。" + "PhotoInfo": { + "type": "object", + "required": ["photo"], + "properties": { + "photo": { + "type": "object", + "description": "撮影された写真データ。", + "title": "写真データ", + "properties": { + "uri": { + "type": "string", + "description": "撮影された写真のURI。", + "title": "URI" + }, + "path": { + "type": "string", + "description": "ファイルが存在するパス。ルートはデバイスプラグインごとに異なる。
File APIで使用可能。", + "title": "ファイルパス" + }, + "mimeType": { + "type": "string", + "description": "撮影された写真のMIME Type。動画・音声を識別するために使用する。", + "title": "MIME Type" + } + }, + "required": ["mimeType", "path"] + } } - } - } - } - }, - "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" + "PreviewGetResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "description": "プレビュー画像URI。形式はJPEGとする。", + "title": "プレビュー画像URI" + } + } + } + ] }, - { - "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。" - } + "PreviewStartResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "description": "開始したプレビューの配信用URI。形式はMotionJPEGとする。", + "title": "プレビュー配信URI" + }, + "streams": { + "type": "array", + "description": "ストリーム情報の配列。", + "title": "ストリームのリスト", + "items": { + "type": "object", + "properties": { + "mimeType": { + "type": "string", + "description": "ストリームのMIMEType。", + "title": "ストリームのMIMEType" + }, + "uri": { + "type": "string", + "description": "ストリームのURI。", + "title": "ストリームのURI" + } + }, + "required": ["mimeType", "uri"] + } + }, + "audio": { + "type": "object", + "description": "音声配信に関する情報。音声のみの配信をサポートしない場合は省略可。", + "title": "音声配信情報", + "properties": { + "uri": { + "type": "string", + "description": "音声配信URI。", + "title": "音声配信URI" + } + }, + "required": ["uri"] + } + } } - } - }, - "audio": { - "type": "object", - "title": "音声配信情報", - "description": "音声配信に関する情報。音声のみの配信をサポートしない場合は省略可。", - "required": [ - "uri" - ], - "properties": { - "uri": { - "type": "string", - "title": "音声配信URI", - "description": "音声配信URI。" + ] + }, + "PreviewStopResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" } - } - } - } - } - ] - }, - "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" + "RecordingStatusResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "$ref": "#/definitions/RecordingStatusInfo" + } + ] }, - { - "$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" + "RecordingStatusEvent": { + "allOf": [ + { + "$ref": "#/definitions/CommonEvent" + }, + { + "$ref": "#/definitions/RecordingStatusInfo" + } + ] }, - { - "type": "object", - "properties": { - "uri": { - "type": "string", - "title": "URI", - "description": "動画または音声のURI。" - }, - "path": { - "type": "string", - "title": "ファイルパス", - "description": "動画または音声へのファイルパス。
ルートはデバイスプラグインごとに異なる。
File APIのパラメータとして使用可能。" + "RecordingStatusInfo": { + "type": "object", + "required": ["media"], + "properties": { + "media": { + "type": "object", + "description": "レコーディング情報", + "title": "レコーディング情報", + "properties": { + "status": { + "type": "string", + "description": "レコーディングの状態を識別する文字列。", + "title": "レコーディングの状態", + "enum": ["recording", "stop", "pause", "resume", "mutetrack", "unmutetrack", "error", "warning"] + }, + "uri": { + "type": "string", + "description": "動画または音声のURI。", + "title": "URI" + }, + "path": { + "type": "string", + "description": "ファイルが存在するパス。ルートはデバイスプラグインごとに違う。", + "title": "ファイルパス" + }, + "mimeType": { + "type": "string", + "description": "録画・録音が開始されたメディアのMIME Type。
このタイプで、動画、音声などを識別する。", + "title": "MIME Type" + }, + "errorMessasge": { + "type": "string", + "description": "エラー、警告内容を伝える文字列。
状態が error、warning の時のみ付加される。省略可能。", + "title": "エラーメッセージ" + } + }, + "required": ["mimeType", "status"] + } } - } - } - ] - }, - "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システムの名前。" + "RecorderStopResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + }, + { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "動画または音声のURI。", + "title": "URI" + }, + "path": { + "type": "string", + "description": "動画または音声へのファイルパス。
ルートはデバイスプラグインごとに異なる。
File APIのパラメータとして使用可能。", + "title": "ファイルパス" + } + } + } + ] }, - "version": { - "type": "string", - "title": "システムバージョン", - "description": "DeviceConnectシステムのバージョン名。" + "RecorderControlResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + } + ] }, - "hmac": { - "type": "string", - "title": "署名", - "description": "レスポンスに対する署名。
アプリケーション側から事前にHMACキーを共有されていた場合は必須。" - } - } - }, - "CommonEvent": { - "type": "object", - "required": [ - "serviceId", - "profile", - "interface", - "attribute" - ], - "properties": { - "serviceId": { - "type": "string", - "title": "サービスID", - "description": "イベントを送信したサービスのID" + "EventRegistrationResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + } + ] }, - "profile": { - "type": "string", - "title": "プロファイル名", - "description": "プロファイル名。" + "EventUnregistrationResponse": { + "allOf": [ + { + "$ref": "#/definitions/CommonResponse" + } + ] }, - "interface": { - "type": "string", - "title": "インターフェース名", - "description": "インターフェース名。" + "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": "署名" + } + } }, - "attribute": { - "type": "string", - "title": "アトリビュート名", - "description": "アトリビュート名。" + "CommonEvent": { + "type": "object", + "required": ["attribute", "interface", "profile", "serviceId"], + "properties": { + "serviceId": { + "type": "string", + "description": "イベントを送信したサービスのID", + "title": "サービスID" + }, + "profile": { + "type": "string", + "description": "プロファイル名。", + "title": "プロファイル名" + }, + "interface": { + "type": "string", + "description": "インターフェース名。", + "title": "インターフェース名" + }, + "attribute": { + "type": "string", + "description": "アトリビュート名。", + "title": "アトリビュート名" + } + } } - } } - } -} +} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/HostDevicePlugin.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/HostDevicePlugin.java index 6e7f354fdd..1347983a87 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/HostDevicePlugin.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/HostDevicePlugin.java @@ -311,7 +311,7 @@ protected void onKeyStoreUpdated(final KeyStore keyStore, final Certificate cert } mSSLContext = createSSLContext(keyStore, DEFAULT_PASSWORD); } catch (Exception e) { - mLogger.log(Level.SEVERE, "Failed to update keystore", e); + mLogger.log(Level.WARNING, "Failed to update keystore", e); } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/HostDevicePluginBindActivity.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/HostDevicePluginBindActivity.java index 2f749976b0..1eaf9da48f 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/HostDevicePluginBindActivity.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/HostDevicePluginBindActivity.java @@ -31,11 +31,6 @@ public class HostDevicePluginBindActivity extends AppCompatActivity { */ private boolean mIsBound = false; - /** - * 画面の回転固定フラグ. - */ - private boolean mDisplayRotationFixed = false; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -179,9 +174,18 @@ public void onServiceDisconnected(ComponentName name) { * @param fixed 固定する場合はtrue、それ以外はfalse */ public void setDisplayRotation(boolean fixed) { - mDisplayRotationFixed = fixed; + setDisplayRotation(fixed, getDisplayOrientation()); + } + + /** + * 画面に表示されている状態で回転を固定します. + * + * @param fixed 固定する場合はtrue、それ以外はfalse + * @param orientation 固定する向き + */ + public void setDisplayRotation(boolean fixed, int orientation) { if (fixed) { - setRequestedOrientation(getDisplayOrientation()); + setRequestedOrientation(orientation); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } @@ -193,14 +197,14 @@ public void setDisplayRotation(boolean fixed) { * @return 画面が固定されている場合はtrue、それ以外はfalse */ public boolean isDisplayRotationFixed() { - return mDisplayRotationFixed; + return getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; } /** * 画面固定を切り替えます. */ public void toggleDisplayRotation() { - setDisplayRotation(!mDisplayRotationFixed); + setDisplayRotation(!isDisplayRotationFixed()); } /** diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/fragment/AlertDialogFragment.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/fragment/AlertDialogFragment.java index 8d802290d7..29b5ac6d5b 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/fragment/AlertDialogFragment.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/fragment/AlertDialogFragment.java @@ -67,7 +67,15 @@ public static AlertDialogFragment create(final String tag, final String title, f * @return AlertDialogFragmentのインスタンス */ public static AlertDialogFragment create(final String tag, final String title, final String message, - final String positive, final String negative) { + final String positive, final String negative) { + Bundle args = createParam(tag, title, message, positive, negative); + AlertDialogFragment dialog = new AlertDialogFragment(); + dialog.setArguments(args); + return dialog; + } + + public static Bundle createParam(final String tag, final String title, final String message, + final String positive, final String negative) { Bundle args = new Bundle(); args.putString(KEY_TAG, tag); args.putString(KEY_TITLE, title); @@ -78,10 +86,7 @@ public static AlertDialogFragment create(final String tag, final String title, f if (negative != null) { args.putString(KEY_NEGATIVE, negative); } - - AlertDialogFragment dialog = new AlertDialogFragment(); - dialog.setArguments(args); - return dialog; + return args; } @Override diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsActivity.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsActivity.java index 7193ce3d41..121aa14074 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsActivity.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsActivity.java @@ -3,11 +3,13 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.content.res.Configuration; import android.os.Bundle; import android.view.MenuItem; import androidx.appcompat.app.ActionBar; +import org.deviceconnect.android.deviceplugin.host.HostDevicePlugin; import org.deviceconnect.android.deviceplugin.host.R; import org.deviceconnect.android.deviceplugin.host.activity.HostDevicePluginBindActivity; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; @@ -17,6 +19,7 @@ public class SettingsActivity extends HostDevicePluginBindActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setRequestedOrientation(getDisplayOrientation()); setContentView(R.layout.activity_recorder_settings); ActionBar actionBar = getSupportActionBar(); @@ -27,9 +30,8 @@ public void onCreate(Bundle savedInstanceState) { } @Override - public void onStart() { - super.onStart(); - setRequestedOrientation(getDisplayOrientation()); + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); } @Override @@ -60,17 +62,20 @@ public int getDisplayOrientation() { return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; } - public HostMediaRecorder getRecorder() { - String recorderId = null; + public String getRecorderId() { Intent intent = getIntent(); if (intent != null) { - recorderId = intent.getStringExtra("recorder_id"); + return intent.getStringExtra("recorder_id"); } - return getHostDevicePlugin().getHostMediaRecorderManager().getRecorder(recorderId); + return null; } - public String getRecorderId() { - return getRecorder().getId(); + public HostMediaRecorder getRecorder() { + HostDevicePlugin plugin = getHostDevicePlugin(); + if (plugin != null) { + return plugin.getHostMediaRecorderManager().getRecorder(getRecorderId()); + } + return null; } public static Intent createSettingsActivityIntent(Context context, String recorderId, Integer rotationFlag) { diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsAudioFragment.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsAudioFragment.java index 9525510825..8e07d50191 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsAudioFragment.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsAudioFragment.java @@ -17,13 +17,16 @@ public class SettingsAudioFragment extends SettingsParameterFragment { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - getPreferenceManager().setSharedPreferencesName(getRecorderId().replaceAll("/", "_")); + getPreferenceManager().setSharedPreferencesName(getRecorderId()); setPreferencesFromResource(R.xml.settings_host_recorder_audio, rootKey); } @Override public void onBindService() { mMediaRecorder = getRecorder(); + if (mMediaRecorder == null) { + return; + } setPreviewAudioSource(mMediaRecorder.getSettings()); setPreviewSampleRate(mMediaRecorder.getSettings()); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsBaseFragment.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsBaseFragment.java index 303eaf112a..6a3e216cd9 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsBaseFragment.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsBaseFragment.java @@ -49,4 +49,17 @@ public String getRecorderId() { } return null; } + + /** + * プレビューサーバ、配信設定ファイル名を取得します. + * + * @return 設定のファイル名 + */ + public String getEncoderId() { + Bundle args = getArguments(); + if (args != null) { + return args.getString("encoder_id"); + } + return null; + } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsBroadcastFragment.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsBroadcastFragment.java index dafd7886c2..41b78986ff 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsBroadcastFragment.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsBroadcastFragment.java @@ -1,15 +1,432 @@ package org.deviceconnect.android.deviceplugin.host.activity.recorder.settings; import android.os.Bundle; +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.host.R; +import org.deviceconnect.android.deviceplugin.host.profile.utils.H264Level; +import org.deviceconnect.android.deviceplugin.host.profile.utils.H264Profile; +import org.deviceconnect.android.deviceplugin.host.profile.utils.H265Level; +import org.deviceconnect.android.deviceplugin.host.profile.utils.H265Profile; +import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; +import org.deviceconnect.android.deviceplugin.host.recorder.util.CapabilityUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; -public class SettingsBroadcastFragment extends SettingsBaseFragment { +public class SettingsBroadcastFragment extends SettingsParameterFragment { + private HostMediaRecorder mMediaRecorder; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - getPreferenceManager().setSharedPreferencesName(getRecorderId().replaceAll("/", "_")); + getPreferenceManager().setSharedPreferencesName(getEncoderId()); setPreferencesFromResource(R.xml.settings_host_recorder_broadcast, rootKey); } + @Override + public void onBindService() { + mMediaRecorder = getRecorder(); + + HostMediaRecorder.EncoderSettings settings = getEncoderSetting(); + + setPreviewSizePreference(settings); + setPreviewVideoEncoderPreference(settings); + setPreviewProfileLevelPreference(settings, settings.getPreviewEncoderName(), false); + + setPreviewCutOutReset(); + setPreviewCutOutSet(); + + 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"); + } + + private HostMediaRecorder.EncoderSettings getEncoderSetting() { + HostMediaRecorder.Settings s = mMediaRecorder.getSettings(); + return s.getEncoderSetting(getEncoderId()); + } + + /** + * 切り抜き範囲の設定にリスナーを設定します. + * + * @param key キー + */ + public void setPreviewClipPreference(String key) { + EditTextPreference pref = findPreference(key); + if (pref != null) { + pref.setOnPreferenceChangeListener(mOnPreferenceChangeListener); + setInputTypeNumber(key); + } + } + + private void setEmptyText(String key) { + EditTextPreference left = findPreference(key); + if (left != null) { + left.setText(null); + } + } + + private void setPreviewClip(String key, Integer value) { + EditTextPreference pref = findPreference(key); + if (pref != null) { + pref.setText(String.valueOf(value)); + } + } + + /** + * 切り抜き範囲のリセットボタンのリスナーを設定します. + */ + private void setPreviewCutOutReset() { + PreferenceScreen pref = findPreference("preview_clip_reset"); + if (pref != null) { + pref.setOnPreferenceClickListener(preference -> { + setEmptyText("preview_clip_left"); + setEmptyText("preview_clip_top"); + setEmptyText("preview_clip_right"); + setEmptyText("preview_clip_bottom"); + getEncoderSetting().setCropRect(null); + return false; + }); + } + } + + private void setPreviewCutOutSet() { + PreferenceScreen pref = findPreference("preview_clip_set"); + if (pref != null) { + pref.setOnPreferenceClickListener(preference -> { + HostMediaRecorder.EncoderSettings settings = getEncoderSetting(); + Size previewSize = settings.getPreviewSize(); + setPreviewClip("preview_clip_left", 0); + setPreviewClip("preview_clip_top", 0); + setPreviewClip("preview_clip_right", previewSize.getWidth()); + setPreviewClip("preview_clip_bottom", previewSize.getHeight()); + return false; + }); + } + } + + /** + * プレビューの解像度 Preference を作成します. + * + * @param settings レコーダの設定 + */ + private void setPreviewSizePreference(HostMediaRecorder.EncoderSettings settings) { + ListPreference pref = findPreference("camera_preview_size"); + if (pref != null) { + List previewSizes = getSupportedPreviewSizes(); + 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(HostMediaRecorder.EncoderSettings 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 レコーダ設定 + * @param videoCodec エンコーダ + * @param reset リセットフラグ + */ + private void setPreviewProfileLevelPreference(HostMediaRecorder.EncoderSettings settings, HostMediaRecorder.VideoCodec videoCodec, boolean reset) { + ListPreference pref = findPreference("preview_profile_level"); + if (pref != null) { + List list = CapabilityUtil.getSupportedProfileLevel(videoCodec.getMimeType()); + if (!list.isEmpty()) { + List entryValues = new ArrayList<>(); + entryValues.add("none"); + + for (HostMediaRecorder.ProfileLevel pl : list) { + String value = getProfileLevel(videoCodec, 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 { + HostMediaRecorder.ProfileLevel pl = settings.getProfileLevel(); + if (pl != null) { + pref.setValue(getProfileLevel(videoCodec, pl)); + } + } + + pref.setVisible(true); + } else { + pref.setEnabled(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 に対応したカメラデバイスがサポートしているプレビューサイズのリストを取得します. + * + * @return サポートしているプレビューサイズのリスト + */ + @NonNull + private List getSupportedPreviewSizes() { + HostMediaRecorder.Settings settings = mMediaRecorder.getSettings(); + 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(HostMediaRecorder.VideoCodec encoderName, HostMediaRecorder.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 HostMediaRecorder.ProfileLevel getProfileLevel(HostMediaRecorder.VideoCodec 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 HostMediaRecorder.ProfileLevel(p.getValue(), l.getValue()); + } + } + case H265: { + H265Profile p = H265Profile.nameOf(profile); + H265Level l = H265Level.nameOf(level); + if (p != null && l != null) { + return new HostMediaRecorder.ProfileLevel(p.getValue(), l.getValue()); + } + } + } + } catch (Exception e) { + return null; + } + } + return null; + } + + /** + * 切り抜き範囲の値を取得します. + * + * 未設定の場合には null を返却します。 + * + * @param key キー + * @return 切り抜き範囲 + */ + private Integer getCropRect(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; + } + + HostMediaRecorder.EncoderSettings settings = getEncoderSetting(); + + String key = preference.getKey(); + if ("camera_preview_size".equals(key)) { + Size size = getSizeFromValue((String) newValue); + if (size != null) { + settings.setPreviewSize(size); + } + } else if ("preview_encoder".equals(key)) { + // エンコーダが切り替えられたので、プロファイル・レベルは一旦削除しておく + try { + settings.setProfileLevel(null); + } catch (Exception e) { + return false; + } + HostMediaRecorder.VideoCodec encoderName = + HostMediaRecorder.VideoCodec.nameOf((String) newValue); + setPreviewProfileLevelPreference(settings, encoderName, true); + } else if ("preview_profile_level".equalsIgnoreCase(key)) { + try { + settings.setProfileLevel(getProfileLevel(settings.getPreviewEncoderName(), (String) newValue)); + } catch (Exception e) { + return false; + } + } else if ("preview_clip_left".equalsIgnoreCase(key)) { + try { + int clipLeft = Integer.parseInt((String) newValue); + Integer clipRight = getCropRect("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 = getCropRect("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 = getCropRect("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 = getCropRect("preview_clip_top"); + if (clipTop != null && clipBottom <= clipTop) { + return false; + } + } catch (NumberFormatException e) { + return false; + } + } + return true; + }; } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsEncoderFragment.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsEncoderFragment.java new file mode 100644 index 0000000000..ada9ec59bd --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsEncoderFragment.java @@ -0,0 +1,488 @@ +package org.deviceconnect.android.deviceplugin.host.activity.recorder.settings; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.util.Size; +import android.widget.Toast; + +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.host.R; +import org.deviceconnect.android.deviceplugin.host.profile.utils.H264Level; +import org.deviceconnect.android.deviceplugin.host.profile.utils.H264Profile; +import org.deviceconnect.android.deviceplugin.host.profile.utils.H265Level; +import org.deviceconnect.android.deviceplugin.host.profile.utils.H265Profile; +import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; +import org.deviceconnect.android.deviceplugin.host.recorder.util.CapabilityUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static androidx.navigation.fragment.NavHostFragment.findNavController; + +public abstract class SettingsEncoderFragment extends SettingsParameterFragment { + private HostMediaRecorder mMediaRecorder; + + @Override + public void onBindService() { + mMediaRecorder = getRecorder(); + if (mMediaRecorder == null) { + findNavController(this).popBackStack(); + return; + } + + HostMediaRecorder.EncoderSettings settings = getEncoderSetting(); + + setPreviewServerPort(); + setPreviewServerUrl(settings.getPort()); + setPreviewSizePreference(settings); + setPreviewVideoEncoderPreference(settings); + setPreviewProfileLevelPreference(settings, settings.getPreviewEncoderName(), false); + + setPreviewCutOutReset(); + setPreviewCutOutSet(); + + 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"); + } + + protected HostMediaRecorder.EncoderSettings getEncoderSetting() { + HostMediaRecorder.Settings s = mMediaRecorder.getSettings(); + return s.getEncoderSetting(getEncoderId()); + } + + private void setPreviewServerPort() { + setInputTypeNumber("port"); + + EditTextPreference pref = findPreference("port"); + if (pref != null) { + pref.setOnPreferenceChangeListener(mOnPreferenceChangeListener); + } + } + + /** + * サーバへのURLを取得します. + * + * @param port ポート番号 + * @return サーバへのURL + */ + protected abstract String getServerUrl(int port); + + /** + * サーバへのURLを設定します. + * + * @param port ポート番号 + */ + private void setPreviewServerUrl(int port) { + PreferenceScreen pref = findPreference("url"); + if (pref != null) { + String url = getServerUrl(port); + pref.setOnPreferenceClickListener(preference -> { + copyToClipboard(requireContext(), "Host Plugin - url", url); + Toast.makeText(requireContext(), R.string.host_recorder_settings_clipboard_copy, Toast.LENGTH_SHORT).show(); + return false; + }); + pref.setSummary(url); + } + } + + /** + * 切り抜き範囲の設定にリスナーを設定します. + * + * @param key キー + */ + public void setPreviewClipPreference(String key) { + EditTextPreference pref = findPreference(key); + if (pref != null) { + pref.setOnPreferenceChangeListener(mOnPreferenceChangeListener); + setInputTypeNumber(key); + } + } + + private void setEmptyText(String key) { + EditTextPreference left = findPreference(key); + if (left != null) { + left.setText(null); + } + } + + private void setPreviewClip(String key, Integer value) { + EditTextPreference pref = findPreference(key); + if (pref != null) { + pref.setText(String.valueOf(value)); + } + } + + /** + * 切り抜き範囲のリセットボタンのリスナーを設定します. + */ + private void setPreviewCutOutReset() { + PreferenceScreen pref = findPreference("preview_clip_reset"); + if (pref != null) { + pref.setOnPreferenceClickListener(preference -> { + setEmptyText("preview_clip_left"); + setEmptyText("preview_clip_top"); + setEmptyText("preview_clip_right"); + setEmptyText("preview_clip_bottom"); + getEncoderSetting().setCropRect(null); + return false; + }); + } + } + + private void setPreviewCutOutSet() { + PreferenceScreen pref = findPreference("preview_clip_set"); + if (pref != null) { + pref.setOnPreferenceClickListener(preference -> { + HostMediaRecorder.EncoderSettings settings = getEncoderSetting(); + Size previewSize = settings.getPreviewSize(); + setPreviewClip("preview_clip_left", 0); + setPreviewClip("preview_clip_top", 0); + setPreviewClip("preview_clip_right", previewSize.getWidth()); + setPreviewClip("preview_clip_bottom", previewSize.getHeight()); + return false; + }); + } + } + + /** + * プレビューの解像度 Preference を作成します. + * + * @param settings レコーダの設定 + */ + private void setPreviewSizePreference(HostMediaRecorder.EncoderSettings settings) { + ListPreference pref = findPreference("camera_preview_size"); + if (pref != null) { + List previewSizes = getSupportedPreviewSizes(); + 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(HostMediaRecorder.EncoderSettings 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 レコーダ設定 + * @param videoCodec エンコーダ + * @param reset リセットフラグ + */ + private void setPreviewProfileLevelPreference(HostMediaRecorder.EncoderSettings settings, HostMediaRecorder.VideoCodec videoCodec, boolean reset) { + ListPreference pref = findPreference("preview_profile_level"); + if (pref != null) { + List list = CapabilityUtil.getSupportedProfileLevel(videoCodec.getMimeType()); + if (!list.isEmpty()) { + List entryValues = new ArrayList<>(); + entryValues.add("none"); + + for (HostMediaRecorder.ProfileLevel pl : list) { + String value = getProfileLevel(videoCodec, 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 { + HostMediaRecorder.ProfileLevel pl = settings.getProfileLevel(); + if (pl != null) { + pref.setValue(getProfileLevel(videoCodec, pl)); + } + } + + pref.setVisible(true); + } else { + pref.setEnabled(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 に対応したカメラデバイスがサポートしているプレビューサイズのリストを取得します. + * + * @return サポートしているプレビューサイズのリスト + */ + @NonNull + private List getSupportedPreviewSizes() { + HostMediaRecorder.Settings settings = mMediaRecorder.getSettings(); + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(getEncoderId()); + List previewSizes = new ArrayList<>(encoderSettings.getSupportedEncoderSizes()); + 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(HostMediaRecorder.VideoCodec encoderName, HostMediaRecorder.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 HostMediaRecorder.ProfileLevel getProfileLevel(HostMediaRecorder.VideoCodec 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 HostMediaRecorder.ProfileLevel(p.getValue(), l.getValue()); + } + } + case H265: { + H265Profile p = H265Profile.nameOf(profile); + H265Level l = H265Level.nameOf(level); + if (p != null && l != null) { + return new HostMediaRecorder.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; + } + + /** + * クリップボードにテキストをコピーします. + * + * @param context コンテキスト + * @param label ラベル + * @param text コピーするテキスト + */ + private static void copyToClipboard(Context context, String label, String text) { + ClipboardManager clipboardManager = + (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboardManager == null) { + return; + } + clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text)); + } + + /** + * 設定が変更された時に呼び出されるリスナー. + */ + private final Preference.OnPreferenceChangeListener mOnPreferenceChangeListener = (preference, newValue) -> { + if (mMediaRecorder == null) { + return false; + } + + HostMediaRecorder.EncoderSettings settings = getEncoderSetting(); + + String key = preference.getKey(); + if ("camera_preview_size".equals(key)) { + Size size = getSizeFromValue((String) newValue); + if (size != null) { + settings.setPreviewSize(size); + } + } else if ("port".equalsIgnoreCase(key)) { + setPreviewServerUrl(Integer.parseInt((String) newValue)); + } else if ("preview_encoder".equals(key)) { + // エンコーダが切り替えられたので、プロファイル・レベルは一旦削除しておく + try { + settings.setProfileLevel(null); + } catch (Exception e) { + return false; + } + HostMediaRecorder.VideoCodec encoderName = + HostMediaRecorder.VideoCodec.nameOf((String) newValue); + setPreviewProfileLevelPreference(settings, encoderName, true); + } else if ("preview_profile_level".equalsIgnoreCase(key)) { + try { + settings.setProfileLevel(getProfileLevel(settings.getPreviewEncoderName(), (String) newValue)); + } catch (Exception e) { + return false; + } + } 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/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsMJPEGFragment.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsMJPEGFragment.java new file mode 100644 index 0000000000..8cca327369 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsMJPEGFragment.java @@ -0,0 +1,41 @@ +package org.deviceconnect.android.deviceplugin.host.activity.recorder.settings; + +import android.os.Bundle; + +import androidx.preference.EditTextPreference; + +import org.deviceconnect.android.deviceplugin.host.R; +import org.deviceconnect.android.deviceplugin.host.activity.fragment.SeekBarDialogPreference; +import org.deviceconnect.android.deviceplugin.host.util.NetworkUtil; + +public class SettingsMJPEGFragment extends SettingsEncoderFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + getPreferenceManager().setSharedPreferencesName(getEncoderId()); + setPreferencesFromResource(R.xml.settings_host_recorder_mjpeg, rootKey); + } + + @Override + public void onBindService() { + super.onBindService(); + setPreviewJpegQuality(); + } + + @Override + protected String getServerUrl(int port) { + String ipAddress = NetworkUtil.getIPAddress(requireContext()); + return "http://" + ipAddress + ":" + port + "/mjpeg"; + } + + /** + * JPEG クオリティを設定します. + */ + private void setPreviewJpegQuality() { + SeekBarDialogPreference pref = findPreference("preview_jpeg_quality"); + if (pref != null) { + pref.setMinValue(0); + pref.setMaxValue(100); + pref.setEnabled(true); + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsMainFragment.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsMainFragment.java index 467f66d6df..017e78a91d 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsMainFragment.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsMainFragment.java @@ -3,8 +3,13 @@ import android.os.Bundle; import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; import org.deviceconnect.android.deviceplugin.host.R; +import org.deviceconnect.android.deviceplugin.host.activity.fragment.AlertDialogFragment; +import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; + +import java.util.List; import static androidx.navigation.fragment.NavHostFragment.findNavController; @@ -22,20 +27,89 @@ public void onResume() { @Override public boolean onPreferenceTreeClick(final Preference preference) { + HostMediaRecorder recorder = getRecorder(); + if (recorder == null) { + showErrorDialog(); + return false; + } + if ("recorder_settings_video".equals(preference.getKey())) { findNavController(this).navigate(R.id.action_main_to_video); } else if ("recorder_settings_audio".equals(preference.getKey())) { findNavController(this).navigate(R.id.action_main_to_audio); - } else if ("recorder_settings_srt".equals(preference.getKey())) { - findNavController(this).navigate(R.id.action_main_to_srt); - } else if ("recorder_settings_broadcast".equals(preference.getKey())) { - findNavController(this).navigate(R.id.action_main_to_broadcast); - } else if ("recorder_settings_port".equals(preference.getKey())) { - findNavController(this).navigate(R.id.action_main_to_port); + } else { + Bundle params = new Bundle(); + params.putString("encoder_id", preference.getKey()); + HostMediaRecorder.Settings settings = recorder.getSettings(); + HostMediaRecorder.EncoderSettings s = settings.getEncoderSetting(preference.getKey()); + if (s != null) { + switch (s.getMimeType()) { + case MJPEG: + findNavController(this).navigate(R.id.action_main_to_mjpeg, params); + break; + case RTSP: + findNavController(this).navigate(R.id.action_main_to_rtsp, params); + break; + case SRT: + findNavController(this).navigate(R.id.action_main_to_srt, params); + break; + case RTMP: + findNavController(this).navigate(R.id.action_main_to_broadcast, params); + break; + } + } } return super.onPreferenceTreeClick(preference); } + @Override + public void onBindService() { + addEncoderList(); + } + + private void showErrorDialog() { + Bundle args = AlertDialogFragment.createParam("error-dialog", + getString(R.string.host_recorder_settings_error_not_found_camera_title), + getString(R.string.host_recorder_settings_error_not_found_camera_message), + getString(R.string.host_recorder_settings_error_not_found_camera_btn), + null); + findNavController(this).navigate(R.id.action_open_error_dialog, args); + } + + private boolean isNotExistPreference(String key) { + return findPreference(key) == null; + } + + private void addEncoderList() { + HostMediaRecorder recorder = getRecorder(); + if (recorder == null) { + return; + } + + HostMediaRecorder.Settings settings = recorder.getSettings(); + List encoderList = settings.getEncoderIdList(); + + PreferenceCategory previewCategory = findPreference("recorder_settings_preview_server"); + PreferenceCategory broadcasterCategory = findPreference("recorder_settings_broadcaster"); + for (String encoderId : encoderList) { + HostMediaRecorder.EncoderSettings encoderSetting = settings.getEncoderSetting(encoderId); + if (isNotExistPreference(encoderId)) { + Preference preference = new Preference(requireContext()); + preference.setTitle(encoderSetting.getName()); + preference.setKey(encoderId); + preference.setIconSpaceReserved(false); + if (encoderSetting.getMimeType() == HostMediaRecorder.MimeType.RTMP) { + broadcasterCategory.addPreference(preference); + } else { + previewCategory.addPreference(preference); + } + } + } + + previewCategory.setVisible(true); + broadcasterCategory.setVisible(true); + } + private void startManager() { SettingsActivity a = (SettingsActivity) getActivity(); if (a != null && !a.isManagerStarted()) { diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsPortFragment.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsPortFragment.java deleted file mode 100644 index 1117c92087..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsPortFragment.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.deviceconnect.android.deviceplugin.host.activity.recorder.settings; - -import android.os.Bundle; - -import org.deviceconnect.android.deviceplugin.host.R; - -public class SettingsPortFragment extends SettingsBaseFragment { - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - getPreferenceManager().setSharedPreferencesName(getRecorderId().replaceAll("/", "_")); - setPreferencesFromResource(R.xml.settings_host_recorder_port, rootKey); - } - - @Override - public void onBindService() { - setInputTypeNumber("mjpeg_port"); - setInputTypeNumber("rtsp_port"); - setInputTypeNumber("srt_port"); - } -} \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsRTSPFragment.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsRTSPFragment.java new file mode 100644 index 0000000000..97edc2d2e6 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsRTSPFragment.java @@ -0,0 +1,22 @@ +package org.deviceconnect.android.deviceplugin.host.activity.recorder.settings; + +import android.os.Bundle; + +import androidx.preference.EditTextPreference; + +import org.deviceconnect.android.deviceplugin.host.R; +import org.deviceconnect.android.deviceplugin.host.util.NetworkUtil; + +public class SettingsRTSPFragment extends SettingsEncoderFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + getPreferenceManager().setSharedPreferencesName(getEncoderId()); + setPreferencesFromResource(R.xml.settings_host_recorder_rtsp, rootKey); + } + + @Override + protected String getServerUrl(int port) { + String ipAddress = NetworkUtil.getIPAddress(requireContext()); + return "rtsp://" + ipAddress + ":" + port; + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsSRTFragment.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsSRTFragment.java index 54a81a4dc8..6e1c971235 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsSRTFragment.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsSRTFragment.java @@ -7,12 +7,12 @@ import androidx.preference.Preference; import org.deviceconnect.android.deviceplugin.host.R; -import org.deviceconnect.android.deviceplugin.host.recorder.util.SRTSettings; +import org.deviceconnect.android.deviceplugin.host.util.NetworkUtil; -public class SettingsSRTFragment extends SettingsBaseFragment { +public class SettingsSRTFragment extends SettingsEncoderFragment { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - getPreferenceManager().setSharedPreferencesName(SRTSettings.FILE_NAME); + getPreferenceManager().setSharedPreferencesName(getEncoderId()); setPreferencesFromResource(R.xml.settings_host_recorder_srt, rootKey); setSummaryOptionAuto(getString(R.string.pref_key_settings_srt_inputbw)); @@ -27,6 +27,12 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setInputTypeNumber(getString(R.string.pref_key_settings_srt_packetfilter)); } + @Override + protected String getServerUrl(int port) { + String ipAddress = NetworkUtil.getIPAddress(requireContext()); + return "srt://" + ipAddress + ":" + port; + } + private void setSummaryOptionAuto(String name) { EditTextPreference inputBwPref = findPreference(name); if (inputBwPref != null) { diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsVideoFragment.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsVideoFragment.java index 68bdd652f6..e6b5940012 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsVideoFragment.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/activity/recorder/settings/SettingsVideoFragment.java @@ -5,50 +5,48 @@ 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.host.R; import org.deviceconnect.android.deviceplugin.host.activity.fragment.SeekBarDialogPreference; import org.deviceconnect.android.deviceplugin.host.profile.utils.AutoExposure; import org.deviceconnect.android.deviceplugin.host.profile.utils.AutoFocus; -import org.deviceconnect.android.deviceplugin.host.profile.utils.H264Level; -import org.deviceconnect.android.deviceplugin.host.profile.utils.H264Profile; -import org.deviceconnect.android.deviceplugin.host.profile.utils.H265Level; -import org.deviceconnect.android.deviceplugin.host.profile.utils.H265Profile; import org.deviceconnect.android.deviceplugin.host.profile.utils.NoiseReduction; import org.deviceconnect.android.deviceplugin.host.profile.utils.OpticalStabilization; import org.deviceconnect.android.deviceplugin.host.profile.utils.Stabilization; import org.deviceconnect.android.deviceplugin.host.profile.utils.WhiteBalance; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; -import org.deviceconnect.android.deviceplugin.host.recorder.util.CapabilityUtil; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import static androidx.navigation.fragment.NavHostFragment.findNavController; + public class SettingsVideoFragment extends SettingsParameterFragment { private HostMediaRecorder mMediaRecorder; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - getPreferenceManager().setSharedPreferencesName(getRecorderId().replaceAll("/", "_")); + getPreferenceManager().setSharedPreferencesName(getRecorderId()); setPreferencesFromResource(R.xml.settings_host_recorder_video, rootKey); } @Override public void onBindService() { mMediaRecorder = getRecorder(); + if (mMediaRecorder == null) { + findNavController(this).popBackStack(); + return; + } HostMediaRecorder.Settings settings = mMediaRecorder.getSettings(); setPictureSizePreference(settings); setPreviewSizePreference(settings); - setPreviewVideoEncoderPreference(settings); - setPreviewProfileLevelPreference(settings, settings.getPreviewEncoderName(), false); + setPreviewFps(settings); setPreviewAutoFocusPreference(settings); setPreviewWhiteBalancePreference(settings); setPreviewWhiteBalanceTemperaturePreference(settings); @@ -61,52 +59,10 @@ public void onBindService() { setPreviewNoiseReduction(settings); setPreviewFocalLength(settings); - setPreviewJpegQuality(); - setPreviewCutOutReset(); - 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"); - } - - /** - * 切り抜き範囲の設定にリスナーを設定します. - * - * @param key キー - */ - public void setPreviewClipPreference(String key) { - EditTextPreference pref = findPreference(key); - if (pref != null) { - pref.setOnPreferenceChangeListener(mOnPreferenceChangeListener); - setInputTypeNumber(key); - } - } - - /** - * 切り抜き範囲のリセットボタンのリスナーを設定します. - */ - private void setPreviewCutOutReset() { - PreferenceScreen pref = findPreference("preview_clip_reset"); - if (pref != null) { - pref.setOnPreferenceClickListener(preference -> { - EditTextPreference left = findPreference("preview_clip_left"); - left.setText(null); - EditTextPreference top = findPreference("preview_clip_top"); - top.setText(null); - EditTextPreference right = findPreference("preview_clip_right"); - right.setText(null); - EditTextPreference bottom = findPreference("preview_clip_bottom"); - bottom.setText(null); - mMediaRecorder.getSettings().setDrawingRange(null); - return false; - }); - } } /** @@ -168,62 +124,28 @@ private void setPreviewSizePreference(HostMediaRecorder.Settings settings) { } } - /** - * エンコーダの設定を行います. - * - * @param settings レコーダ設定 - */ - private void setPreviewVideoEncoderPreference(HostMediaRecorder.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 レコーダ設定 - * @param encoderName エンコーダ - * @param reset リセットフラグ - */ - private void setPreviewProfileLevelPreference(HostMediaRecorder.Settings settings, HostMediaRecorder.VideoEncoderName encoderName, boolean reset) { - ListPreference pref = findPreference("preview_profile_level"); + private void setPreviewFps(HostMediaRecorder.Settings settings) { + ListPreference pref = findPreference("camera_fps"); if (pref != null) { - List list = CapabilityUtil.getSupportedProfileLevel(encoderName.getMimeType()); - if (!list.isEmpty()) { + List> fpsList = settings.getSupportedFps(); + if (!fpsList.isEmpty()) { List entryValues = new ArrayList<>(); - entryValues.add("none"); - - for (HostMediaRecorder.ProfileLevel pl : list) { - String value = getProfileLevel(encoderName, pl); - if (value != null) { - entryValues.add(value); - } + for (Range fps : fpsList) { + entryValues.add(fps.getLower() + "-" + fps.getUpper()); } pref.setEntries(entryValues.toArray(new String[0])); pref.setEntryValues(entryValues.toArray(new String[0])); pref.setOnPreferenceChangeListener(mOnPreferenceChangeListener); - if (reset) { - pref.setValue("none"); - } else { - HostMediaRecorder.ProfileLevel pl = settings.getProfileLevel(); - if (pl != null) { - pref.setValue(getProfileLevel(encoderName, pl)); - } + Range previewFps = settings.getPreviewFps(); + if (previewFps != null) { + pref.setValue(previewFps.getLower() + "-" + previewFps.getUpper()); } - +// int previewSize = settings.getPreviewMaxFrameRate(); +// if (previewSize != null) { +// pref.setValue(getValueFromSize(previewSize)); +// } pref.setVisible(true); } else { pref.setEnabled(false); @@ -521,18 +443,6 @@ private void setPreviewFocalLength(HostMediaRecorder.Settings settings) { } } - /** - * JPEG クオリティを設定します. - */ - private void setPreviewJpegQuality() { - SeekBarDialogPreference pref = findPreference("preview_jpeg_quality"); - if (pref != null) { - pref.setMinValue(0); - pref.setMaxValue(100); - pref.setEnabled(true); - } - } - /** * サイズの小さい方からソートを行うための比較演算子. */ @@ -606,60 +516,13 @@ private Size getSizeFromValue(String value) { return null; } - /** - * プロファイルとレベルを文字列に変換します. - * - * @param encoderName エンコーダ - * @param pl プロファイルとレベル - * @return 文字列 - */ - private String getProfileLevel(HostMediaRecorder.VideoEncoderName encoderName, HostMediaRecorder.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 HostMediaRecorder.ProfileLevel getProfileLevel(HostMediaRecorder.VideoEncoderName encoderName, String value) { + private Range getRangeFromValue(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); - return new HostMediaRecorder.ProfileLevel(p.getValue(), l.getValue()); - } - case H265: { - H265Profile p = H265Profile.nameOf(profile); - H265Level l = H265Level.nameOf(level); - return new HostMediaRecorder.ProfileLevel(p.getValue(), l.getValue()); - } - } + int w = Integer.parseInt(t[0].trim()); + int h = Integer.parseInt(t[1].trim()); + return new Range<>(w, h); } catch (Exception e) { return null; } @@ -667,26 +530,6 @@ private HostMediaRecorder.ProfileLevel getProfileLevel(HostMediaRecorder.VideoEn 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; - } - /** * 設定が変更された時に呼び出されるリスナー. */ @@ -708,53 +551,10 @@ private Integer getDrawingRange(String key) { if (size != null) { settings.setPreviewSize(size); } - } else if ("preview_encoder".equals(key)) { - // エンコーダが切り替えられたので、プロファイル・レベルは一旦削除しておく - settings.setProfileLevel(null); - HostMediaRecorder.VideoEncoderName encoderName = - HostMediaRecorder.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; + } else if ("camera_fps".equals(key)) { + Range fps = getRangeFromValue((String) newValue); + if (fps != null) { + settings.setPreviewFps(fps); } } return true; diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/camera/Camera2Helper.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/camera/Camera2Helper.java index 75daa0d193..f90cb8d687 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/camera/Camera2Helper.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/camera/Camera2Helper.java @@ -49,7 +49,7 @@ public static int getSensorOrientation(final CameraManager cameraManager, final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); Integer sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); return sensorOrientation == null ? 0 : sensorOrientation; - } catch (CameraAccessException e) { + } catch (Exception e) { return 0; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/camera/CameraWrapperManager.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/camera/CameraWrapperManager.java index ae49eaa14d..97a714da3a 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/camera/CameraWrapperManager.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/camera/CameraWrapperManager.java @@ -14,6 +14,7 @@ import org.deviceconnect.android.deviceplugin.host.BuildConfig; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -31,27 +32,17 @@ public class CameraWrapperManager { * カメラ操作クラスの一覧. */ private final Map mCameras = new LinkedHashMap<>(); + private final Context mContext; /** * コンストラクタ. * @param context コンテキスト */ public CameraWrapperManager(final Context context) { + mContext = context; try { - CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); - for (String cameraId : cameraManager.getCameraIdList()) { - CameraWrapper camera = null; - try { - camera = new CameraWrapper(context, cameraId); - mCameras.put(cameraId, camera); - } catch (Exception e) { - // ignore. - if (BuildConfig.DEBUG) { - Log.w("CameraWrapperManager", "Failed to create a CameraWrapper.", e); - } - } - } - } catch (CameraAccessException e) { + loadCamera(); + } catch (Exception e) { // No camera is available now. } } @@ -64,6 +55,67 @@ public synchronized List getCameraList() { return new ArrayList<>(mCameras.values()); } + /** + * 指定された ID のカメラを取得する. + * + * @param cameraId カメラの識別子 + * @return CameraWrapper、存在しない場合は null + */ + public synchronized CameraWrapper getCameraById(String cameraId) { + return mCameras.get(cameraId); + } + + /** + * ロストしたカメラを削除します. + * + * @throws CameraAccessException カメラへのアクセス失敗した場合に発生 + */ + private void removeLostCamera() throws CameraAccessException { + CameraManager cameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); + List cameraIdList = Arrays.asList(cameraManager.getCameraIdList()); + for (String key : mCameras.keySet()) { + if (cameraIdList.contains(key)) { + continue; + } + CameraWrapper cameraWrapper = mCameras.remove(key); + if (cameraWrapper != null) { + cameraWrapper.destroy(); + } + } + } + + /** + * カメラを読み込みます. + * + * @throws CameraAccessException カメラへのアクセス失敗した場合に発生 + */ + private void loadCamera() throws CameraAccessException { + CameraManager cameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); + for (String cameraId : cameraManager.getCameraIdList()) { + if (mCameras.containsKey(cameraId)) { + continue; + } + + try { + mCameras.put(cameraId, new CameraWrapper(mContext, cameraId)); + } catch (Exception e) { + // ignore. + } + } + } + + /** + * カメラを再読み込みする. + */ + public synchronized void reload() { + try { + removeLostCamera(); + loadCamera(); + } catch (Exception e) { + // No camera is available now. + } + } + /** * カメラ操作クラスを全て破棄する. * アプリケーションを終了するときにのみ実行すること. diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostConnectionManager.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostConnectionManager.java index 7b7d0841a4..55f15dede9 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostConnectionManager.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostConnectionManager.java @@ -16,6 +16,7 @@ import android.net.NetworkInfo; import android.net.NetworkRequest; import android.net.Uri; +import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Bundle; @@ -39,10 +40,13 @@ import org.deviceconnect.android.libmedia.streaming.util.WeakReferenceList; import org.deviceconnect.android.message.DevicePluginContext; import org.deviceconnect.android.util.NotificationUtils; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; +import javax.sql.ConnectionEventListener; + public class HostConnectionManager { private static final int NOTIFICATION_ID = 3527; @@ -52,6 +56,7 @@ public class HostConnectionManager { private final ConnectivityManager mConnectivityManager; private TelephonyManager mTelephonyManager; private NetworkType mMobileNetworkType = NetworkType.TYPE_NONE; + private int mStrengthLevel = 0; private HostTrafficMonitor mTrafficMonitor; private final Handler mCallbackHandler = new Handler(Looper.getMainLooper()); private final WeakReferenceList mConnectionEventListeners = new WeakReferenceList<>(); @@ -118,8 +123,8 @@ public void onDisplayInfoChanged(@NonNull TelephonyDisplayInfo info) { public void onSignalStrengthsChanged(SignalStrength signalStrength) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { for (CellSignalStrength strength : signalStrength.getCellSignalStrengths()) { - int level = strength.getLevel(); - switch (level) { + mStrengthLevel = strength.getLevel(); + switch (mStrengthLevel) { case CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN: case CellSignalStrength.SIGNAL_STRENGTH_POOR: case CellSignalStrength.SIGNAL_STRENGTH_MODERATE: @@ -128,7 +133,10 @@ public void onSignalStrengthsChanged(SignalStrength signalStrength) { break; } } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mStrengthLevel = signalStrength.getLevel(); } + postOnChangeNetwork(); } }; @@ -185,9 +193,17 @@ public void removeConnectionEventListener(ConnectionEventListener listener) { * * @return 有効になっているネットワーク名 */ - public String getActivityNetworkString() { + public String getActivityNetworkString(NetworkType networkType) { Context context = mPluginContext.getContext(); - switch (getActivityNetwork()) { + if (context == null) { + return "No Connect"; + } + + if (networkType == null) { + return context.getString(R.string.host_connection_network_type_no_connect); + } + + switch (networkType) { case TYPE_MOBILE: return context.getString(R.string.host_connection_network_type_mobile); case TYPE_WIFI: @@ -213,30 +229,11 @@ public String getActivityNetworkString() { /** * 有効になっているネットワークタイプを取得します. * - * @return ネットワークタイプ + * @return ネットワークの情報 */ - public NetworkType getActivityNetwork() { - NetworkType networkType = NetworkType.TYPE_NONE; - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo(); - if (networkInfo != null) { - switch (networkInfo.getType()) { - case ConnectivityManager.TYPE_MOBILE: - networkType = NetworkType.TYPE_MOBILE; - break; - case ConnectivityManager.TYPE_WIFI: - networkType = NetworkType.TYPE_WIFI; - break; - case ConnectivityManager.TYPE_ETHERNET: - networkType = NetworkType.TYPE_ETHERNET; - break; - case ConnectivityManager.TYPE_BLUETOOTH: - networkType = NetworkType.TYPE_BLUETOOTH; - break; - } - } - } else { + public NetworkCaps getNetworkCaps() { + NetworkCaps caps = new NetworkCaps(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Network n = mConnectivityManager.getActiveNetwork(); NetworkCapabilities capabilities = mConnectivityManager.getNetworkCapabilities(n); if (capabilities != null) { @@ -245,26 +242,119 @@ public NetworkType getActivityNetwork() { boolean isBluetooth = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH); boolean isEthernet = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET); if (isWifi) { - networkType = NetworkType.TYPE_WIFI; + caps.mStrengthLevel = getWifiStrengthLevel(); + caps.mType = NetworkType.TYPE_WIFI; } else if (isEthernet) { - networkType = NetworkType.TYPE_ETHERNET; + caps.mType = NetworkType.TYPE_ETHERNET; } else if (isBluetooth) { - networkType = NetworkType.TYPE_BLUETOOTH; + caps.mType = NetworkType.TYPE_BLUETOOTH; } else if (isMobile) { - networkType = NetworkType.TYPE_MOBILE; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + caps.mStrengthLevel = capabilities.getSignalStrength(); + } else { + caps.mStrengthLevel = mStrengthLevel; + } + caps.mType = NetworkType.TYPE_MOBILE; if (mMobileNetworkType != null && mMobileNetworkType != NetworkType.TYPE_NONE) { - networkType = mMobileNetworkType; + caps.mType = mMobileNetworkType; } } else { - networkType = NetworkType.TYPE_NONE; + caps.mType = NetworkType.TYPE_NONE; if (mMobileNetworkType != null && mMobileNetworkType != NetworkType.TYPE_NONE) { - networkType = mMobileNetworkType; + caps.mType = mMobileNetworkType; } } + caps.mDownstreamBW = capabilities.getLinkDownstreamBandwidthKbps(); + caps.mUpstreamBW = capabilities.getLinkUpstreamBandwidthKbps(); } + } else { + NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo(); + if (networkInfo != null) { + switch (networkInfo.getType()) { + case ConnectivityManager.TYPE_MOBILE: + caps.mStrengthLevel = mStrengthLevel; + caps.mType = NetworkType.TYPE_MOBILE; + break; + case ConnectivityManager.TYPE_WIFI: + caps.mStrengthLevel = getWifiStrengthLevel(); + caps.mType = NetworkType.TYPE_WIFI; + break; + case ConnectivityManager.TYPE_ETHERNET: + caps.mType = NetworkType.TYPE_ETHERNET; + break; + case ConnectivityManager.TYPE_BLUETOOTH: + caps.mType = NetworkType.TYPE_BLUETOOTH; + break; + } + } + } + return caps; + } + + /** + * ネットワークの情報を格納するクラス. + */ + public class NetworkCaps { + private NetworkType mType = NetworkType.TYPE_NONE; + private int mUpstreamBW; + private int mDownstreamBW; + private int mStrengthLevel; + + /** + * ネットワークタイプを取得します. + * + * @return ネットワークタイプ + */ + public NetworkType getType() { + return mType; + } + + /** + * ネットワークタイプの文字列を取得します. + * + * @return ネットワークタイプ + */ + public String getTypeString() { + return getActivityNetworkString(mType); + } + + /** + * upstream の帯域幅を取得します. + * + * @return upstream の帯域幅 + */ + public int getUpstreamBW() { + return mUpstreamBW; + } + + /** + * downstream の帯域幅を取得します. + * + * @return downstream の帯域幅 + */ + public int getDownstreamBW() { + return mDownstreamBW; } - return networkType; + /** + * 電波強度を取得します. + * + * @return 電波強度 + */ + public int getStrengthLevel() { + return mStrengthLevel; + } + + @NotNull + @Override + public String toString() { + return "NetworkCaps{" + + "mType=" + mType + + ", mUpstreamBW=" + mUpstreamBW + + ", mDownstreamBW=" + mDownstreamBW + + ", mStrengthLevel=" + mStrengthLevel + + '}'; + } } public enum NetworkType { @@ -282,23 +372,21 @@ public enum NetworkType { private boolean mRegisterTelephonyManager; private synchronized void registerTelephony() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (mRegisterTelephonyManager) { - return; - } + if (mRegisterTelephonyManager) { + return; + } - try { - mTelephonyManager = (TelephonyManager) mPluginContext.getContext() - .getSystemService(Context.TELEPHONY_SERVICE); - if (mTelephonyManager != null) { - int events = PhoneStateListener.LISTEN_SIGNAL_STRENGTHS; - events |= PhoneStateListener.LISTEN_DISPLAY_INFO_CHANGED; - mTelephonyManager.listen(mPhoneStateListener, events); - mRegisterTelephonyManager = true; - } - } catch (Exception e) { - // ignore. + try { + mTelephonyManager = (TelephonyManager) mPluginContext.getContext() + .getSystemService(Context.TELEPHONY_SERVICE); + if (mTelephonyManager != null) { + int events = PhoneStateListener.LISTEN_SIGNAL_STRENGTHS; + events |= PhoneStateListener.LISTEN_DISPLAY_INFO_CHANGED; + mTelephonyManager.listen(mPhoneStateListener, events); + mRegisterTelephonyManager = true; } + } catch (Exception e) { + // ignore. } } @@ -337,14 +425,14 @@ private void registerNetworkCallback() { * ネットワークの接続イベントを受信するための Receiver を解除します. */ private void unregisterNetworkCallback() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - try { + try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { mPluginContext.getContext().unregisterReceiver(mHostConnectionReceiver); - } catch (Exception e) { - // ignore. + } else { + mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); } - } else { - mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + } catch (Exception e) { + // ignore. } } @@ -357,6 +445,34 @@ public boolean isWifiEnabled() { return mWifiManager != null && mWifiManager.isWifiEnabled(); } + /** + * 接続している WiFi の電波強度を取得します. + * + * WiFi の RSSI から下記の範囲で値を返却します。 + * 優れた> -50 dBm + * 良好-50〜-60 dBm + * 普通-60〜-70 dBm + * 弱い<-70dBm + * + * @return WiFi の電波強度 + */ + public int getWifiStrengthLevel() { + WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + if (wifiInfo != null) { + int rssi = wifiInfo.getRssi(); + if (rssi > -50) { + return 4; + } else if (rssi > -60) { + return 3; + } else if (rssi > -70) { + return 2; + } else { + return 1; + } + } + return 0; + } + /** * WiFi接続の状態を設定する. * @@ -468,7 +584,20 @@ public synchronized void stopTrafficMonitor() { } /** - * ネットワーク通信量のリストを取得します. + * 指定されたネットワークの通信量履歴を取得します. + * + * @param networkType ネットワークタイプ + * @return ネットワークの通信量履歴 + */ + public synchronized List getTrafficList(int networkType) { + if (mTrafficMonitor != null) { + return mTrafficMonitor.getTrafficList(networkType); + } + return new ArrayList<>(); + } + + /** + * ネットワーク毎の最新の通信量をリストにして取得します. * * リストには、各ネットワークの通信量が格納されています。 * @@ -476,8 +605,8 @@ public synchronized void stopTrafficMonitor() { * * @return ネットワーク通信量のリスト */ - public synchronized List getTrafficList() { - return mTrafficMonitor != null ? mTrafficMonitor.getTrafficList() : new ArrayList<>(); + public synchronized List getLastTrafficForAllNetwork() { + return mTrafficMonitor != null ? mTrafficMonitor.getLastTrafficForAllNetwork() : new ArrayList<>(); } /** @@ -547,10 +676,14 @@ public static boolean checkUsageAccessSettings(Context context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } - AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, - android.os.Process.myUid(), context.getPackageName()); - return mode == AppOpsManager.MODE_ALLOWED; + try { + AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, + android.os.Process.myUid(), context.getPackageName()); + return mode == AppOpsManager.MODE_ALLOWED; + } catch (Exception e) { + return false; + } } /** @@ -663,8 +796,18 @@ public interface PermissionCallback { void onDisallowed(); } + /** + * 処理結果を通知するコールバック. + */ public interface Callback { + /** + * 処理に成功した場合に呼び出します. + */ void onSuccess(); + + /** + * 処理に失敗した場合に呼び出します. + */ void onFailure(); } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostTraffic.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostTraffic.java index 84cccdfb26..b028db8d01 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostTraffic.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostTraffic.java @@ -6,6 +6,8 @@ public class HostTraffic { long mTx; long mBitrateRx; long mBitrateTx; + long mStartTime; + long mEndTime; /** * ネットワークタイプを取得します. @@ -52,12 +54,32 @@ public long getBitrateTx() { return mBitrateTx; } + /** + * 計測開始時間を取得します. + * + * @return 計測開始時間 + */ + public long getStartTime() { + return mStartTime; + } + + /** + * 計測終了時間を取得します. + * + * @return 計測終了時間 + */ + public long getEndTime() { + return mEndTime; + } + @Override public String toString() { - return "networkType: " + mNetworkType + "\n" - + "rx: " + mRx + "\n" - + "tx: " + mTx + "\n" - + "BitrateRx: " + mBitrateRx + "\n" - + "BitrateTx: " + mBitrateTx + "\n"; + return "{\n" + + " networkType: " + mNetworkType + "\n" + + " rx: " + mRx + "\n" + + " tx: " + mTx + "\n" + + " BitrateRx: " + mBitrateRx + "\n" + + " BitrateTx: " + mBitrateTx + "\n" + + "}"; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostTrafficMonitor.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostTrafficMonitor.java index 6c0e56a4c3..087cbba15d 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostTrafficMonitor.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/connection/HostTrafficMonitor.java @@ -29,6 +29,7 @@ class HostTrafficMonitor { private final Map> mStatsMap = new HashMap<>(); private final long mInterval; private Timer mTimer; + private int mCacheSize = 50; private NetworkStatsManager mNetworkStatsManager; private OnTrafficListener mOnTrafficListener; @@ -50,6 +51,15 @@ class HostTrafficMonitor { } } + /** + * 通信量の情報をキャッシュするサイズを設定します. + * + * @param cacheSize キャッシュサイズ + */ + void setCacheSize(int cacheSize) { + mCacheSize = cacheSize; + } + /** * 通信量を通知するリスナーを設定します. * @@ -85,40 +95,71 @@ void stopTimer() { } /** - * ネットワークごとの通信量を取得します. + * Stats の情報を HostTraffic に変換します. * * @param networkType ネットワークタイプ - * @return ネットワークごとの通信量 + * @param stats 通信情報 + * @param pre 通信情報 + * @return HostTraffic */ - private HostTraffic getTraffic(int networkType) { + private HostTraffic conv(int networkType, Stats stats, Stats pre) { + long rx = (stats.getTotalRxBytes() - pre.getTotalRxBytes()); + long tx = (stats.getTotalTxBytes() - pre.getTotalTxBytes()); + long bitrateRx = 8 * rx / ((stats.getEndTime() - pre.getEndTime()) / 1000); + long bitrateTx = 8 * tx / ((stats.getEndTime() - pre.getEndTime()) / 1000); + HostTraffic traffic = new HostTraffic(); + traffic.mNetworkType = networkType; + traffic.mRx = rx; + traffic.mTx = tx; + traffic.mBitrateRx = bitrateRx; + traffic.mBitrateTx = bitrateTx; + traffic.mStartTime = stats.mEndTime - mInterval; + traffic.mEndTime = stats.mEndTime; + return traffic; + } + + /** + * 指定されたネットワークの最新の通信量を取得します. + * + * @param networkType ネットワークタイプ + * @return 指定されたネットワークの最新の通信量 + */ + private HostTraffic getLastTraffic(int networkType) { List statsList = mStatsMap.get(networkType); if (statsList != null && statsList.size() > 1) { Stats pre = statsList.get(statsList.size() - 2); Stats stats = statsList.get(statsList.size() - 1); - long rx = (stats.getTotalRxBytes() - pre.getTotalRxBytes()); - long tx = (stats.getTotalTxBytes() - pre.getTotalTxBytes()); - long bitrateRx = 8 * rx / ((stats.getEndTime() - pre.getEndTime()) / 1000); - long bitrateTx = 8 * tx / ((stats.getEndTime() - pre.getEndTime()) / 1000); - HostTraffic traffic = new HostTraffic(); - traffic.mNetworkType = networkType; - traffic.mRx = rx; - traffic.mTx = tx; - traffic.mBitrateRx = bitrateRx; - traffic.mBitrateTx = bitrateTx; - return traffic; + return conv(networkType, stats, pre); } return null; } /** - * ネットワークの通信量を取得します. + * 指定されたネットワークの通信量のリストを取得します. + * + * @param networkType ネットワークタイプ + * @return 指定されたネットワークの通信量のリスト + */ + List getTrafficList(int networkType) { + List trafficList = new ArrayList<>(); + List statsList = mStatsMap.get(networkType); + for (int i = 1; i < statsList.size(); i++) { + Stats pre = statsList.get(i - 1); + Stats stats = statsList.get(i); + trafficList.add(conv(networkType, stats, pre)); + } + return trafficList; + } + + /** + * 各ネットワーク毎の最新の通信量をリストに格納して返却します. * - * @return ネットワークの通信量 + * @return 各ネットワーク毎の最新の通信量 */ - List getTrafficList() { + List getLastTrafficForAllNetwork() { List trafficList = new ArrayList<>(); for (int networkType : NETWORK_TYPE_LIST) { - HostTraffic traffic = getTraffic(networkType); + HostTraffic traffic = getLastTraffic(networkType); if (traffic != null) { trafficList.add(traffic); } @@ -144,6 +185,12 @@ private void monitoring() { } } + /** + * 指定されたネットワークタイプの通信情報を取得します. + * + * @param networkType ネットワークタイプ + * @return ネットワーク情報 + */ private HostTraffic getNetworkStats(int networkType) { HostTraffic traffic = null; @@ -163,12 +210,12 @@ private HostTraffic getNetworkStats(int networkType) { statsList.add(stats); - if (statsList.size() > 100) { + if (statsList.size() > mCacheSize) { statsList.remove(0); } if (statsList.size() > 1) { - traffic = getTraffic(networkType); + traffic = getLastTraffic(networkType); } return traffic; diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostCameraProfile.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostCameraProfile.java index ecfa77b208..f4d59bb66e 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostCameraProfile.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostCameraProfile.java @@ -322,7 +322,7 @@ public boolean onRequest(final Intent request, final Intent response) { } if (sensorSensitivity != null) { - if (!settings.isSupportedSensorSensorSensitivity(sensorSensitivity)) { + if (!settings.isSupportedSensorSensitivity(sensorSensitivity)) { MessageUtils.setInvalidRequestParameterError(response, "sensorSensitivity is invalid."); return true; } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostConnectionProfile.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostConnectionProfile.java index 242200ded4..b5afc62129 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostConnectionProfile.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostConnectionProfile.java @@ -28,7 +28,13 @@ import org.deviceconnect.message.DConnectMessage; import org.deviceconnect.message.intent.message.IntentDConnectMessage; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.Locale; /** * Connection プロファイル. @@ -277,7 +283,6 @@ public boolean onRequest(final Intent request, final Intent response) { // GET /gotapi/connection/network private final DConnectApi mGetNetworkApi = new GetApi() { - @Override public String getAttribute() { return "network"; @@ -289,7 +294,8 @@ public boolean onRequest(final Intent request, final Intent response) { @Override public void onAllowed() { setResult(response, DConnectMessage.RESULT_OK); - response.putExtra("network", mHostConnectionManager.getActivityNetworkString()); + + response.putExtra("network", createNetworkCaps()); sendResponse(response); } @@ -389,42 +395,76 @@ public String getAttribute() { @Override public boolean onRequest(final Intent request, final Intent response) { - if (HostConnectionManager.checkUsageAccessSettings(getContext())) { - List trafficList = mHostConnectionManager.getTrafficList(); - for (HostTraffic traffic : trafficList) { - response.putExtra(convertNetworkTypeToString( - traffic.getNetworkType()), createNetworkBitrate(traffic)); + String sinceString = request.getStringExtra("since"); + String untilString = request.getStringExtra("until"); + Long since = null; + Long until = null; + + if (sinceString != null) { + try { + since = df.parse(sinceString).getTime(); + } catch (ParseException e) { + // ignore. } - setResult(response, DConnectMessage.RESULT_OK); - return true; - } else { + } + + if (untilString != null) { + try { + until = df.parse(untilString).getTime(); + } catch (ParseException e) { + // ignore. + } + } + + if (!HostConnectionManager.checkUsageAccessSettings(getContext())) { HostConnectionManager.openUsageAccessSettings(getContext()); - // 使用履歴が有効になるのをポーリングしながら待機 - for (int i = 0; i < 10; i++) { + // 使用履歴が有効になるのをポーリングしながら待機します。 + for (int i = 0; i < 30; i++) { try { - Thread.sleep(3000); + Thread.sleep(1000); } catch (InterruptedException e) { break; } + if (HostConnectionManager.checkUsageAccessSettings(getContext())) { break; } } + } - if (HostConnectionManager.checkUsageAccessSettings(getContext())) { - List trafficList = mHostConnectionManager.getTrafficList(); + if (HostConnectionManager.checkUsageAccessSettings(getContext())) { + final int[] networkTypeList = { + ConnectivityManager.TYPE_MOBILE, + ConnectivityManager.TYPE_WIFI + }; + + for (int networkType : networkTypeList) { + List trafficList = mHostConnectionManager.getTrafficList(networkType); + ArrayList trafficArray = new ArrayList<>(); for (HostTraffic traffic : trafficList) { - response.putExtra(convertNetworkTypeToString( - traffic.getNetworkType()), createNetworkBitrate(traffic)); + if (until != null) { + if (until < traffic.getStartTime()) { + continue; + } + } + + if (since != null) { + if (traffic.getEndTime() < since) { + continue; + } + } + + trafficArray.add(createNetworkBitrate(traffic)); } - setResult(response, DConnectMessage.RESULT_OK); - } else { - MessageUtils.setIllegalServerStateError(response, "Failed to start collecting a traffic."); + response.putExtra(convertNetworkTypeToString(networkType), trafficArray); } - - return true; + setResult(response, DConnectMessage.RESULT_OK); + } else { + MessageUtils.setIllegalServerStateError(response, "Failed to start collecting a traffic."); } + + return true; } }; @@ -565,6 +605,10 @@ private String convertNetworkTypeToString(int networkType) { return "mobile"; case ConnectivityManager.TYPE_WIFI: return "wifi"; + case ConnectivityManager.TYPE_BLUETOOTH: + return "bluetooth"; + case ConnectivityManager.TYPE_ETHERNET: + return "ethernet"; default: return "unknown"; } @@ -582,9 +626,60 @@ private Bundle createNetworkBitrate(HostTraffic traffic) { Bundle data = new Bundle(); data.putBundle("send", send); data.putBundle("receive", receive); + data.putString("start", df.format(new Date(traffic.getStartTime()))); + data.putString("end", df.format(new Date(traffic.getEndTime()))); return data; } + private final DateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()); + + private HostTraffic getNetworkBitrate(int type) { + List trafficList = mHostConnectionManager.getLastTrafficForAllNetwork(); + for (HostTraffic traffic : trafficList) { + if (type == traffic.getNetworkType()) { + return traffic; + } + } + return null; + } + + private Bundle createNetworkCaps() { + HostConnectionManager.NetworkCaps networkCaps = mHostConnectionManager.getNetworkCaps(); + Bundle network = new Bundle(); + network.putString("type", networkCaps.getTypeString()); + network.putInt("strengthLevel", networkCaps.getStrengthLevel()); + network.putInt("upstream", networkCaps.getUpstreamBW()); + network.putInt("downstream", networkCaps.getDownstreamBW()); + + if (HostConnectionManager.checkUsageAccessSettings(getContext())) { + switch (networkCaps.getType()) { + case TYPE_BLUETOOTH: + case TYPE_ETHERNET: + break; + case TYPE_WIFI: + { + HostTraffic traffic = getNetworkBitrate(ConnectivityManager.TYPE_WIFI); + if (traffic != null) { + network.putBundle("traffic", createNetworkBitrate(traffic)); + } + } break; + case TYPE_MOBILE: + case TYPE_LTE_CA: + case TYPE_NR_NSA: + case TYPE_NR_NSA_MMWAV: + case TYPE_LTE_ADVANCED_PRO: + { + HostTraffic traffic = getNetworkBitrate(ConnectivityManager.TYPE_MOBILE); + if (traffic != null) { + network.putBundle("traffic", createNetworkBitrate(traffic)); + } + } break; + } + } + + return network; + } + /** * ネットワークが変更されたことを通知します. */ @@ -592,10 +687,11 @@ private void postOnChangeNetwork() { List events = EventManager.INSTANCE.getEventList(HostDevicePlugin.SERVICE_ID, HostConnectionProfile.PROFILE_NAME, "network", "onChange"); + Bundle network = createNetworkCaps(); for (int i = 0; i < events.size(); i++) { Event event = events.get(i); Intent intent = EventManager.createEventMessage(event); - intent.putExtra("network", mHostConnectionManager.getActivityNetworkString()); + intent.putExtra("network", network); sendEvent(intent, event.getAccessToken()); } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostLiveStreamingProfile.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostLiveStreamingProfile.java index 41ae5a36d6..00a89c7e13 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostLiveStreamingProfile.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostLiveStreamingProfile.java @@ -3,14 +3,14 @@ import android.content.Intent; import android.os.Bundle; import android.util.Log; +import android.util.Range; import android.util.Size; import org.deviceconnect.android.BuildConfig; -import org.deviceconnect.android.deviceplugin.host.recorder.Broadcaster; import org.deviceconnect.android.deviceplugin.host.recorder.BroadcasterProvider; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorderManager; -import org.deviceconnect.android.deviceplugin.host.recorder.PreviewServer; +import org.deviceconnect.android.deviceplugin.host.recorder.LiveStreaming; import org.deviceconnect.android.event.Event; import org.deviceconnect.android.event.EventError; import org.deviceconnect.android.event.EventManager; @@ -22,6 +22,7 @@ import org.deviceconnect.android.profile.api.PutApi; import org.deviceconnect.message.DConnectMessage; +import java.util.ArrayList; import java.util.List; public class HostLiveStreamingProfile extends DConnectProfile { @@ -59,7 +60,7 @@ public void onConfigChanged(HostMediaRecorder recorder) { } @Override - public void onPreviewStarted(HostMediaRecorder recorder, List servers) { + public void onPreviewStarted(HostMediaRecorder recorder, List servers) { } @Override @@ -71,17 +72,21 @@ public void onPreviewError(HostMediaRecorder recorder, Exception e) { } @Override - public void onBroadcasterStarted(HostMediaRecorder recorder, Broadcaster broadcaster) { - postOnStart(broadcaster); + public void onBroadcasterStarted(HostMediaRecorder recorder, List broadcasters) { + if (mHostMediaRecorder == null) { + mHostMediaRecorder = recorder; + } + postOnStart(broadcasters); } @Override - public void onBroadcasterStopped(HostMediaRecorder recorder, Broadcaster broadcaster) { - postOnStop(broadcaster); + public void onBroadcasterStopped(HostMediaRecorder recorder) { + postOnStop(recorder.getBroadcasterProvider().getLiveStreamingList()); + mHostMediaRecorder = null; } @Override - public void onBroadcasterError(HostMediaRecorder recorder, Broadcaster broadcaster, Exception e) { + public void onBroadcasterError(HostMediaRecorder recorder, LiveStreaming broadcaster, Exception e) { postOnError(broadcaster); } @@ -105,6 +110,14 @@ public void onRecordingResume(HostMediaRecorder recorder) { public void onRecordingStopped(HostMediaRecorder recorder, String fileName) { } + @Override + public void onFoundRecorder(HostMediaRecorder recorder) { + } + + @Override + public void onLostRecorder(HostMediaRecorder recorder) { + } + @Override public void onError(HostMediaRecorder recorder, Exception e) { } @@ -158,16 +171,47 @@ public boolean onRequest(final Intent request, final Intent response) { HostMediaRecorder.Settings settings = recorder.getSettings(); try { + if (broadcastURI != null) { + for (String encoderId : recorder.getSettings().getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = recorder.getSettings().getEncoderSetting(encoderId); + if (encoderSettings != null && encoderSettings.getMimeType().equals(HostMediaRecorder.MimeType.RTMP)) { + encoderSettings.setBroadcastURI(broadcastURI); + } + } + } + if (width != null && height != null) { settings.setPreviewSize(new Size(width, height)); + + for (String encoderId : recorder.getSettings().getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = recorder.getSettings().getEncoderSetting(encoderId); + if (encoderSettings != null && encoderSettings.getMimeType().equals(HostMediaRecorder.MimeType.RTMP)) { + encoderSettings.setPreviewSize(new Size(width, height)); + } + } } if (bitrate != null) { - settings.setPreviewBitRate(bitrate); + for (String encoderId : recorder.getSettings().getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = recorder.getSettings().getEncoderSetting(encoderId); + if (encoderSettings != null && encoderSettings.getMimeType().equals(HostMediaRecorder.MimeType.RTMP)) { + encoderSettings.setPreviewBitRate(bitrate); + } + } } if (frameRate != null) { - settings.setPreviewMaxFrameRate(frameRate); + Range fps = recorder.getSettings().getPreviewFpsFromFrameRate(frameRate); + if (fps != null) { + settings.setPreviewFps(fps); + } + + for (String encoderId : recorder.getSettings().getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = recorder.getSettings().getEncoderSetting(encoderId); + if (encoderSettings != null && encoderSettings.getMimeType().equals(HostMediaRecorder.MimeType.RTMP)) { + encoderSettings.setPreviewMaxFrameRate(frameRate); + } + } } // 映像が有効な時の音声設定を行う @@ -186,8 +230,8 @@ public boolean onRequest(final Intent request, final Intent response) { public void onAllowed() { mHostMediaRecorder = recorder; - Broadcaster broadcaster = recorder.startBroadcaster(broadcastURI); - if (broadcaster != null) { + List broadcasters = recorder.startBroadcaster(broadcastURI); + if (!broadcasters.isEmpty() ) { setResult(response, DConnectMessage.RESULT_OK); } else { MessageUtils.setUnknownError(response, "Failed to start a live streaming."); @@ -242,18 +286,16 @@ public String getAttribute() { @Override public boolean onRequest(final Intent request, final Intent response) { - Bundle streaming; + List streaming = new ArrayList<>(); if (mHostMediaRecorder != null) { BroadcasterProvider provider = mHostMediaRecorder.getBroadcasterProvider(); - if (provider != null && provider.isRunning()) { - streaming = createStreamingBundle(provider.getBroadcaster(), "streaming"); - } else { - streaming = createStreamingBundle(null, "stop"); + for (LiveStreaming broadcaster : provider.getLiveStreamingList()) { + streaming.add(createStreamingBundle(broadcaster, broadcaster.isRunning() ? "streaming" : "stop")); } } else { - streaming = createStreamingBundle(null, "stop"); + streaming.add(createStreamingBundle(null, "stop")); } - response.putExtra(PARAM_KEY_STREAMING, streaming); + response.putExtra(PARAM_KEY_STREAMING, streaming.toArray(new Bundle[0])); setResult(response, DConnectMessage.RESULT_OK); return true; @@ -396,10 +438,10 @@ private HostMediaRecorder getHostMediaRecorder(String video, String audio) { return mHostMediaRecorderManager.getRecorder(audio); } - private Bundle createStreamingBundle(Broadcaster broadcaster, String status) { + private Bundle createStreamingBundle(LiveStreaming broadcaster, String status) { Bundle streaming = new Bundle(); if (broadcaster != null) { - streaming.putString(PARAM_KEY_URI, broadcaster.getBroadcastURI()); + streaming.putString(PARAM_KEY_URI, broadcaster.getUri()); } streaming.putString(PARAM_KEY_STATUS, status); @@ -409,37 +451,47 @@ private Bundle createStreamingBundle(Broadcaster broadcaster, String status) { video.putString(PARAM_KEY_URI, mHostMediaRecorder.getId()); video.putInt(PARAM_KEY_WIDTH, settings.getPreviewSize().getWidth()); video.putInt(PARAM_KEY_HEIGHT, settings.getPreviewSize().getHeight()); - video.putInt(PARAM_KEY_BITRATE, settings.getPreviewBitRate()); - video.putInt(PARAM_KEY_FRAME_RATE, settings.getPreviewMaxFrameRate()); +// video.putInt(PARAM_KEY_BITRATE, settings.getPreviewBitRate()); +// video.putInt(PARAM_KEY_FRAME_RATE, settings.getPreviewMaxFrameRate()); video.putString(PARAM_KEY_MIME_TYPE, broadcaster.getMimeType()); streaming.putParcelable(PARAM_KEY_VIDEO, video); } return streaming; } - private void postOnStart(Broadcaster broadcaster) { + private void postOnStart(List broadcasters) { List evtList = EventManager.INSTANCE.getEventList(getService().getId(), PROFILE_NAME, null, AT_ON_STATUS_CHANGE); + List b = new ArrayList<>(); + for (LiveStreaming broadcaster : broadcasters) { + b.add(createStreamingBundle(broadcaster, "normal")); + } + for (Event event : evtList) { Intent intent = EventManager.createEventMessage(event); - intent.putExtra(PARAM_KEY_STREAMING, createStreamingBundle(broadcaster, "normal")); + intent.putExtra(PARAM_KEY_STREAMING, b.toArray(new Bundle[0])); sendEvent(intent, event.getAccessToken()); } } - private void postOnStop(Broadcaster broadcaster) { + private void postOnStop(List broadcasters) { List evtList = EventManager.INSTANCE.getEventList(getService().getId(), PROFILE_NAME, null, AT_ON_STATUS_CHANGE); + List b = new ArrayList<>(); + for (LiveStreaming broadcaster : broadcasters) { + b.add(createStreamingBundle(broadcaster, "stop")); + } + for (Event event : evtList) { Intent intent = EventManager.createEventMessage(event); - intent.putExtra(PARAM_KEY_STREAMING, createStreamingBundle(broadcaster, "stop")); + intent.putExtra(PARAM_KEY_STREAMING, b.toArray(new Bundle[0])); sendEvent(intent, event.getAccessToken()); } } - private void postOnError(Broadcaster broadcaster) { + private void postOnError(LiveStreaming broadcaster) { List evtList = EventManager.INSTANCE.getEventList(getService().getId(), PROFILE_NAME, null, AT_ON_STATUS_CHANGE); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostMediaStreamingRecordingProfile.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostMediaStreamingRecordingProfile.java index da1726c848..17c8a6a422 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostMediaStreamingRecordingProfile.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/profile/HostMediaStreamingRecordingProfile.java @@ -17,12 +17,13 @@ import org.deviceconnect.android.deviceplugin.host.profile.utils.H264Profile; import org.deviceconnect.android.deviceplugin.host.profile.utils.H265Level; import org.deviceconnect.android.deviceplugin.host.profile.utils.H265Profile; -import org.deviceconnect.android.deviceplugin.host.recorder.Broadcaster; +import org.deviceconnect.android.deviceplugin.host.recorder.AbstractMediaRecorder; +import org.deviceconnect.android.deviceplugin.host.recorder.CropInterface; import org.deviceconnect.android.deviceplugin.host.recorder.HostDevicePhotoRecorder; import org.deviceconnect.android.deviceplugin.host.recorder.HostDeviceStreamRecorder; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorderManager; -import org.deviceconnect.android.deviceplugin.host.recorder.PreviewServer; +import org.deviceconnect.android.deviceplugin.host.recorder.LiveStreaming; import org.deviceconnect.android.deviceplugin.host.recorder.camera.Camera2Recorder; import org.deviceconnect.android.deviceplugin.host.recorder.util.CapabilityUtil; import org.deviceconnect.android.event.Event; @@ -30,7 +31,6 @@ import org.deviceconnect.android.event.EventManager; 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.PostApi; @@ -60,747 +60,1242 @@ public class HostMediaStreamingRecordingProfile extends MediaStreamRecordingProf */ private final FileManager mFileManager; - // GET /gotapi/mediaStreamRecording/mediaRecorder - private final DConnectApi mGetMediaRecorderApi = new GetApi() { + private final HostMediaRecorderManager.OnEventListener mOnEventListener = new HostMediaRecorderManager.OnEventListener() { + @Override + public void onMuteChanged(HostMediaRecorder recorder, boolean mute) { + } + @Override - public String getAttribute() { - return ATTRIBUTE_MEDIARECORDER; + public void onConfigChanged(HostMediaRecorder recorder) { } @Override - public boolean onRequest(final Intent request, final Intent response) { - String target = getTarget(request); + public void onPreviewStarted(HostMediaRecorder recorder, List servers) { + } - final HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; + @Override + public void onPreviewStopped(HostMediaRecorder recorder) { + } + + @Override + public void onPreviewError(HostMediaRecorder recorder, Exception e) { + } + + @Override + public void onBroadcasterStarted(HostMediaRecorder recorder, List broadcasters) { + } + + @Override + public void onBroadcasterStopped(HostMediaRecorder recorder) { + } + + @Override + public void onBroadcasterError(HostMediaRecorder recorder, LiveStreaming broadcaster, Exception e) { + } + + @Override + public void onTakePhoto(HostMediaRecorder recorder, String uri, String filePath, String mimeType) { + sendEventForTakePhoto(getService().getId(), uri, filePath, mimeType); + } + + @Override + public void onRecordingStarted(HostMediaRecorder recorder, String fileName) { + sendEventForRecordingChange(getService().getId(), recorder.getState(), + mFileManager.getContentUri() + "/" + fileName, + "/" + fileName, recorder.getMimeType(), null); + } + + @Override + public void onRecordingPause(HostMediaRecorder recorder) { + } + + @Override + public void onRecordingResume(HostMediaRecorder recorder) { + } + + @Override + public void onRecordingStopped(HostMediaRecorder recorder, String fileName) { + sendEventForRecordingChange(getService().getId(), recorder.getState(), + mFileManager.getContentUri() + "/" + fileName, + "/" + fileName, recorder.getMimeType(), null); + } + + @Override + public void onFoundRecorder(HostMediaRecorder recorder) { + } + + @Override + public void onLostRecorder(HostMediaRecorder recorder) { + } + + @Override + public void onError(HostMediaRecorder recorder, Exception e) { + } + }; + + public HostMediaStreamingRecordingProfile(final HostMediaRecorderManager mgr, final FileManager fileMgr) { + mFileManager = fileMgr; + mRecorderMgr = mgr; + mRecorderMgr.addOnEventListener(mOnEventListener); + + // GET /gotapi/mediaStreamRecording/mediaRecorder + addApi(new GetApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_MEDIARECORDER; } - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - List recorders = new LinkedList<>(); - for (HostMediaRecorder recorder : mRecorderMgr.getRecorders()) { - HostMediaRecorder.Settings settings = recorder.getSettings(); + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); - Bundle info = new Bundle(); - setRecorderId(info, recorder.getId()); - setRecorderName(info, recorder.getName()); - setRecorderMIMEType(info, recorder.getMimeType()); + final HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } - if (recorder.getState() == HostMediaRecorder.State.RECORDING) { - setRecorderState(info, RecorderState.RECORDING); + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + List recorders = new LinkedList<>(); + if (target == null) { + for (HostMediaRecorder recorder : mRecorderMgr.getRecorders()) { + recorders.add(createMediaRecorderInfo(recorder)); + } + setRecorders(response, recorders.toArray(new Bundle[0])); } else { - setRecorderState(info, RecorderState.INACTIVE); + response.putExtras(createMediaRecorderInfo(recorder)); } + setResult(response, DConnectMessage.RESULT_OK); + sendResponse(response); + } - if (recorder.getMimeType().startsWith("image/") || recorder.getMimeType().startsWith("video/")) { - // 静止画の設定 - Size pictureSize = settings.getPictureSize(); - if (pictureSize != null) { - setRecorderImageWidth(info, pictureSize.getWidth()); - setRecorderImageHeight(info, pictureSize.getHeight()); - } + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); + return false; + } + }); - // プレビュー設定 - Size previewSize = settings.getPreviewSize(); - if (previewSize != null) { - setRecorderPreviewWidth(info, previewSize.getWidth()); - setRecorderPreviewHeight(info, previewSize.getHeight()); - setRecorderPreviewMaxFrameRate(info, settings.getPreviewMaxFrameRate()); - info.putInt("previewBitRate", settings.getPreviewBitRate() / 1024); - info.putInt("previewKeyFrameInterval", settings.getPreviewKeyFrameInterval()); - info.putString("previewEncoder", settings.getPreviewEncoder()); - HostMediaRecorder.ProfileLevel pl = settings.getProfileLevel(); - if (pl != null) { - switch (HostMediaRecorder.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; - } - } - info.putFloat("previewJpegQuality", settings.getPreviewQuality() / 100.0f); + // GET /gotapi/mediaStreamRecording/options + addApi(new GetApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_OPTIONS; + } - Bundle status = new Bundle(); - status.putBoolean("preview", recorder.isPreviewRunning()); - status.putBoolean("broadcast", recorder.isBroadcasterRunning()); - status.putBoolean("recording", recorder.getState() == HostMediaRecorder.State.RECORDING); - info.putParcelable("status", status); - } + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } - // 切り抜き設定 - 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); + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + if (target == null) { + List recorders = new LinkedList<>(); + for (HostMediaRecorder recorder : mRecorderMgr.getRecorders()) { + recorders.add(createRecorderOption(recorder)); } - } else if (recorder.getMimeType().startsWith("audio/")) { - // 音声の設定 + setRecorders(response, recorders.toArray(new Bundle[0])); + } else { + response.putExtras(createRecorderOption(recorder)); } + setResult(response, DConnectMessage.RESULT_OK); + sendResponse(response); + } - setRecorderConfig(info, ""); - recorders.add(info); + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); } - setRecorders(response, recorders.toArray(new Bundle[recorders.size()])); + }); + + return false; + } + }); + + // 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); + + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } + + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + setOptions(request, response); + sendResponse(response); + } + + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); + return false; + } + }); + + // PUT /gotapi/mediaStreamRecording/onPhoto + addApi(new PutApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_ON_PHOTO; + } + + @Override + public boolean onRequest(final Intent request, final Intent response) { + EventError error = EventManager.INSTANCE.addEvent(request); + if (error == EventError.NONE) { setResult(response, DConnectMessage.RESULT_OK); - sendResponse(response); + } else { + setResult(response, DConnectMessage.RESULT_ERROR); } + return true; + } + }); + + // DELETE /gotapi/mediaStreamRecording/onPhoto + addApi(new DeleteApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_ON_PHOTO; + } - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); + @Override + public boolean onRequest(final Intent request, final Intent response) { + EventError error = EventManager.INSTANCE.removeEvent(request); + if (error == EventError.NONE) { + setResult(response, DConnectMessage.RESULT_OK); + } else { + setResult(response, DConnectMessage.RESULT_ERROR); } - }); - return false; - } - }; + return true; + } + }); - // GET /gotapi/mediaStreamRecording/options - private final DConnectApi mGetOptionsApi = new GetApi() { - @Override - public String getAttribute() { - return ATTRIBUTE_OPTIONS; - } + // POST /gotapi/mediaStreamRecording/takePhoto + addApi(new PostApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_TAKE_PHOTO; + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - String target = getTarget(request); + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); - final HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } + + // TODO 他のカメラとは排他的に処理を行うようにします。 + if (!mRecorderMgr.canUseRecorder(recorder)) { + // 他のカメラが使用中の場合はエラーを返却 + MessageUtils.setIllegalDeviceStateError(response, "Other cameras are being used."); + return true; + } + + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + // TODO 他のカメラとは排他的に処理を行うようにします。 + if (recorder instanceof Camera2Recorder) { + // 使用する予定のレコーダがカメラの場合は、使用していない他のカメラを停止する + mRecorderMgr.stopCameraRecorder(recorder); + } + + recorder.takePhoto(new HostDevicePhotoRecorder.OnPhotoEventListener() { + @Override + public void onTakePhoto(final String uri, final String filePath, final String mimeType) { + setResult(response, DConnectMessage.RESULT_OK); + setUri(response, uri); + setPath(response, filePath); + sendResponse(response); + } + + @Override + public void onFailedTakePhoto(final String errorMessage) { + MessageUtils.setUnknownError(response, errorMessage); + sendResponse(response); + } + }); + } + + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); + + return false; } + }); + + // POST /gotapi/mediaStreamRecording/record + addApi(new PostApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_RECORD; + } + + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - HostMediaRecorder.Settings settings = recorder.getSettings(); - if (recorder.getMimeType().startsWith("image/") || recorder.getMimeType().startsWith("video/")) { - // 映像系の設定 - setSupportedImageSizes(response, settings.getSupportedPictureSizes()); - setSupportedPreviewSizes(response, settings.getSupportedPreviewSizes()); - setSupportedVideoEncoders(response, settings.getSupportedVideoEncoders()); - setSupportedFps(response, settings.getSupportedFps()); - } else if (recorder.getMimeType().startsWith("audio/")) { - // 音声系の設定 + // TODO 他のカメラとは排他的に処理を行うようにします。 + if (!mRecorderMgr.canUseRecorder(recorder)) { + // 他のカメラが使用中の場合はエラーを返却 + MessageUtils.setIllegalDeviceStateError(response, "Other cameras are being used."); + return true; + } + + // 撮影がすでに行われている場合はエラーを返却 + if (recorder.getState() != HostMediaRecorder.State.INACTIVE + && recorder.getState() != HostMediaRecorder.State.PREVIEW) { + MessageUtils.setIllegalDeviceStateError(response, + recorder.getName() + " is already running."); + return true; + } + + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + recorder.startRecording(new HostDeviceStreamRecorder.RecordingCallback() { + @Override + public void onRecorded(final HostDeviceStreamRecorder streamRecorder, final String fileName) { + setResult(response, DConnectMessage.RESULT_OK); + setPath(response, "/" + fileName); + setUri(response, mFileManager.getContentUri() + "/" + fileName); + sendResponse(response); + } + + @Override + public void onFailed(final HostDeviceStreamRecorder recorder, final String errorMessage) { + MessageUtils.setIllegalServerStateError(response, errorMessage); + sendResponse(response); + } + }); + } + + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); + return false; + } + }); + + // PUT /gotapi/mediaStreamRecording/stop + addApi(new PutApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_STOP; + } + + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } + + if (recorder.getState() == HostMediaRecorder.State.INACTIVE) { + MessageUtils.setIllegalDeviceStateError(response, "recorder is stopped already."); + return true; + } + + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + recorder.stopRecording(new HostDeviceStreamRecorder.StoppingCallback() { + @Override + public void onStopped(HostDeviceStreamRecorder streamRecorder, String fileName) { + setResult(response, DConnectMessage.RESULT_OK); + setPath(response, "/" + fileName); + setUri(response, mFileManager.getContentUri() + "/" + fileName); + sendResponse(response); + } + + @Override + public void onFailed(HostDeviceStreamRecorder recorder, String errorMessage) { + MessageUtils.setIllegalServerStateError(response, errorMessage); + sendResponse(response); + } + }); + } + + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); + + return false; + } + }); + + // PUT /gotapi/mediaStreamRecording/pause + addApi(new PutApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_PAUSE; + } + + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } + + if (!recorder.canPauseRecording()) { + MessageUtils.setNotSupportAttributeError(response); + return true; + } + + if (recorder.getState() != HostMediaRecorder.State.RECORDING) { + MessageUtils.setIllegalDeviceStateError(response); + return true; + } + + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + recorder.pauseRecording(); + + setResult(response, DConnectMessage.RESULT_OK); + sendResponse(response); + } + + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); + + return false; + } + }); + + // PUT /gotapi/mediaStreamRecording/resume + addApi(new PutApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_RESUME; + } + + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } + + if (!recorder.canPauseRecording()) { + MessageUtils.setNotSupportAttributeError(response); + return true; + } + + if (recorder.getState() != HostMediaRecorder.State.PAUSED) { + MessageUtils.setIllegalDeviceStateError(response); + return true; + } + + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + recorder.resumeRecording(); + setResult(response, DConnectMessage.RESULT_OK); + sendResponse(response); + } + + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); } + }); + + return false; + } + }); + // PUT /gotapi/mediaStreamRecording/onRecordingChange + addApi(new PutApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_ON_RECORDING_CHANGE; + } + + @Override + public boolean onRequest(final Intent request, final Intent response) { + EventError error = EventManager.INSTANCE.addEvent(request); + if (error == EventError.NONE) { setResult(response, DConnectMessage.RESULT_OK); - setMIMEType(response, recorder.getSupportedMimeTypes()); + } else { + setResult(response, DConnectMessage.RESULT_ERROR); + } + return true; + } + }); + + // DELETE /gotapi/mediaStreamRecording/onRecordingChange + addApi(new DeleteApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_ON_RECORDING_CHANGE; + } - sendResponse(response); + @Override + public boolean onRequest(final Intent request, final Intent response) { + EventError error = EventManager.INSTANCE.removeEvent(request); + if (error == EventError.NONE) { + setResult(response, DConnectMessage.RESULT_OK); + } else { + setResult(response, DConnectMessage.RESULT_ERROR); } + 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); - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; } - }); - return false; - } - }; + // TODO 他のカメラとは排他的に処理を行うようにします。 + if (!mRecorderMgr.canUseRecorder(recorder)) { + // 他のカメラが使用中の場合はエラーを返却 + MessageUtils.setIllegalDeviceStateError(response, "Other cameras are being used."); + return true; + } - // PUT /gotapi/mediaStreamRecording/options - private final DConnectApi mPutOptionsApi = new PutApi() { - private void setOptions(final Intent request, final Intent response) { - String target = getTarget(request); - String mimeType = getMIMEType(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"); - HostMediaRecorder.ProfileLevel profileLevel = null; - - HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return; + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + HostDevicePlugin plugin = getHostDevicePlugin(); + plugin.getSSLContext(sslContext -> { + recorder.setSSLContext(sslContext); + + List servers = recorder.startPreview(); + if (servers.isEmpty()) { + MessageUtils.setIllegalServerStateError(response, "Failed to start web server."); + } else { + String defaultUri = null; + List streams = new ArrayList<>(); + for (LiveStreaming server : servers) { + // Motion-JPEG をデフォルトの値として使用します + if (defaultUri == null && server.getUri().startsWith("http://") + && "video/x-mjpeg".equals(server.getMimeType())) { + defaultUri = server.getUri(); + } + + Bundle stream = new Bundle(); + stream.putString("mimeType", server.getMimeType()); + stream.putString("uri", server.getUri()); + streams.add(stream); + } + setResult(response, DConnectMessage.RESULT_OK); + setUri(response, defaultUri != null ? defaultUri : ""); + response.putExtra("streams", streams.toArray(new Bundle[0])); + } + + sendResponse(response); + }); + } + + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); + + return false; + } + }); + + // DELETE /gotapi/mediaStreamRecording/preview + addApi(new DeleteApi() { + @Override + public String getAttribute() { + return ATTRIBUTE_PREVIEW; } - HostMediaRecorder.Settings settings = recorder.getSettings(); + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); - if (!isSupportedMimeType(recorder, mimeType)) { - MessageUtils.setInvalidRequestParameterError(response, "MIME-Type " + mimeType + " is unsupported."); - return; + final HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } + + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + recorder.stopPreview(); + setResult(response, DConnectMessage.RESULT_OK); + sendResponse(response); + } + + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); + return false; } + }); - if (recorder.getState() != HostMediaRecorder.State.INACTIVE - && recorder.getState() != HostMediaRecorder.State.PREVIEW) { - MessageUtils.setInvalidRequestParameterError(response, "settings of active target cannot be changed."); - return; + // POST /gotapi/mediaStreamRecording/preview/requestKeyFrame + addApi(new PostApi() { + @Override + public String getInterface() { + return "preview"; + } + + @Override + public String getAttribute() { + return "requestKeyFrame"; } - // 値の妥当性チェック + @Override + public boolean onRequest(final Intent request, final Intent response) { + final String target = getTarget(request); - if (imageWidth != null || imageHeight != null) { - if (imageWidth == null) { - MessageUtils.setInvalidRequestParameterError(response, "imageWidth is not set."); - return; + final HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; } - if (imageHeight == null) { - MessageUtils.setInvalidRequestParameterError(response, "imageHeight is not set."); - return; + if (!recorder.isBroadcasterRunning() && !recorder.isPreviewRunning()) { + MessageUtils.setIllegalServerStateError(response, "Recorder has not started previewing."); + } else { + recorder.requestKeyFrame(); + setResult(response, DConnectMessage.RESULT_OK); } - if (!settings.isSupportedPictureSize(imageWidth, imageHeight)) { - MessageUtils.setInvalidRequestParameterError(response, "Unsupported image size: imageWidth = " - + imageWidth + ", imageHeight = " + imageHeight); - return; - } + return true; } + }); - if (previewWidth != null || previewHeight != null) { - if (previewWidth == null) { - MessageUtils.setInvalidRequestParameterError(response, "previewWidth is not set."); - return; - } - - if (previewHeight == null) { - MessageUtils.setInvalidRequestParameterError(response, "previewHeight is not set."); - return; - } - - if (!settings.isSupportedPreviewSize(previewWidth, previewHeight)) { - MessageUtils.setInvalidRequestParameterError(response, "Unsupported preview size: previewWidth = " - + previewWidth + ", previewHeight = " + previewHeight); - return; - } + // PUT /gotapi/mediaStreamRecording/preview/mute + addApi(new PutApi() { + @Override + public String getInterface() { return ATTRIBUTE_PREVIEW; } + @Override + public String getAttribute() { + return "mute"; } - if (previewEncoder != null) { - if (!settings.isSupportedVideoEncoder(previewEncoder)) { - MessageUtils.setInvalidRequestParameterError(response, - "Unsupported preview encoder: " + previewEncoder); - return; - } + @Override + public boolean onRequest(final Intent request, final Intent response) { + return setMute(true, request, response); } + }); - if (previewProfile != null || previewLevel != null) { - if (previewProfile == null) { - MessageUtils.setInvalidRequestParameterError(response, - "previewProfile is not set."); - return; - } + // DELETE /gotapi/mediaStreamRecording/preview/mute + addApi(new DeleteApi() { - if (previewLevel == null) { - MessageUtils.setInvalidRequestParameterError(response, - "previewLevel is not set."); - return; - } + @Override + public String getInterface() { return ATTRIBUTE_PREVIEW; } + @Override + public String getAttribute() { + return "mute"; + } - switch (settings.getPreviewEncoderName()) { - 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; - } - profileLevel = new HostMediaRecorder.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; - } - profileLevel = new HostMediaRecorder.ProfileLevel(p.getValue(), l.getValue()); - } break; - } + @Override + public boolean onRequest(final Intent request, final Intent response) { + return setMute(false, request, response); } + }); - if (previewJpegQuality != null) { - if (previewJpegQuality < 0.0 || previewJpegQuality > 1.0) { - MessageUtils.setInvalidRequestParameterError(response, - "previewJpegQuality is invalid. value=" + previewJpegQuality); - return; - } + // PUT /gotapi/mediaStreamRecording/encoder + addApi(new PutApi() { + @Override + public String getAttribute() { + return "encoder"; } - if (previewClipLeft != null || previewClipTop != null - || previewClipRight != null || previewClipBottom != null) { - if (previewClipLeft == null) { - MessageUtils.setInvalidRequestParameterError(response, "previewClipLeft is not set."); - return; - } + @Override + public boolean onRequest(Intent request, Intent response) { + String target = getTarget(request); + String mimeTypeValue = request.getStringExtra("mimeType"); + String name = request.getStringExtra("name"); + Integer width = parseInteger(request, "width"); + Integer height = parseInteger(request, "height"); + Integer frameRate = parseInteger(request, "frameRate"); + Integer bitRate = parseInteger(request, "bitRate"); + Integer keyFrameInterval = parseInteger(request, "keyFrameInterval"); + String codec = request.getStringExtra("codec"); + String profile = request.getStringExtra("profile"); + String level = request.getStringExtra("level"); + Integer intraRefresh = parseInteger(request,"intraRefresh"); + Boolean useSoftwareEncoder = parseBoolean(request,"useSoftwareEncoder"); + Double jpegQuality = parseDouble(request, "jpegQuality"); + String broadcastUri = request.getStringExtra("broadcastUri"); + Integer retryCount = parseInteger(request, "retryCount"); + Integer retryInterval = parseInteger(request, "retryInterval"); - if (previewClipTop == null) { - MessageUtils.setInvalidRequestParameterError(response, "previewClipTop is not set."); - return; + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, + "target is invalid."); + return true; } - if (previewClipRight == null) { - MessageUtils.setInvalidRequestParameterError(response, "previewClipRight is not set."); - return; - } + List encoderIdList = getEncoderSettings(recorder, name, false); - if (previewClipBottom == null) { - MessageUtils.setInvalidRequestParameterError(response, "previewClipBottom is not set."); - return; + if (width != null && width < 0) { + MessageUtils.setInvalidRequestParameterError(response, + "width is invalid. value=" + width); + return true; } - if (previewClipLeft < 0) { + if (height != null && height < 0) { MessageUtils.setInvalidRequestParameterError(response, - "previewClipLeft cannot set a negative value."); - return; + "height is invalid. value=" + height); + return true; } - if (previewClipBottom < 0) { + if (frameRate != null && frameRate <= 0) { MessageUtils.setInvalidRequestParameterError(response, - "previewClipBottom cannot set a negative value."); - return; + "frameRate is invalid. value=" + frameRate); + return true; } - if (previewClipLeft >= previewClipRight) { + if (bitRate != null && bitRate <= 0) { MessageUtils.setInvalidRequestParameterError(response, - "previewClipLeft is larger than previewClipRight."); - return; + "bitRate. value=" + bitRate); + return true; } - if (previewClipTop >= previewClipBottom) { + if (keyFrameInterval != null && keyFrameInterval < 0) { MessageUtils.setInvalidRequestParameterError(response, - "previewClipTop is larger than previewClipBottom."); - return; + "keyFrameInterval. value=" + keyFrameInterval); + return true; } - } - // 値の設定 + if (intraRefresh != null && intraRefresh < 0) { + MessageUtils.setInvalidRequestParameterError(response, + "intraRefresh is invalid. value=" + intraRefresh); + return true; + } - if (imageWidth != null && imageHeight != null) { - settings.setPictureSize(new Size(imageWidth, imageHeight)); - } + if (jpegQuality != null) { + if (jpegQuality < 0.0 || jpegQuality > 1.0) { + MessageUtils.setInvalidRequestParameterError(response, + "jpegQuality is invalid. value=" + jpegQuality); + return true; + } + } - if (previewWidth != null && previewHeight != null) { - settings.setPreviewSize(new Size(previewWidth, previewHeight)); - } + if (retryCount != null) { + if (retryCount < 0) { + MessageUtils.setInvalidRequestParameterError(response, + "retryCount is invalid. value=" + retryCount); + return true; + } + } - if (previewMaxFrameRate != null) { - settings.setPreviewMaxFrameRate(previewMaxFrameRate.intValue()); - } + if (retryInterval != null) { + if (retryInterval <= 0) { + MessageUtils.setInvalidRequestParameterError(response, + "retryInterval is invalid. value=" + retryInterval); + return true; + } + } - if (previewBitRate != null) { - settings.setPreviewBitRate(previewBitRate * 1024); - } + HostMediaRecorder.ProfileLevel profileLevel = convertProfileLevel(recorder, codec, profile, level); - if (previewKeyFrameInterval != null) { - settings.setPreviewKeyFrameInterval(previewKeyFrameInterval); - } + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + HostMediaRecorder.Settings settings = recorder.getSettings(); + boolean isChangeConfig = false; + + for (String encoderId : encoderIdList) { + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(encoderId); + if (encoderSettings == null) { + if (mimeTypeValue == null) { + MessageUtils.setInvalidRequestParameterError(response, "mimeType is not set."); + sendResponse(response); + return; + } - if (previewEncoder != null) { - settings.setPreviewEncoder(previewEncoder); - // エンコーダが切り替えられた場合は、プロファイル・レベルは設定無しにする - settings.setProfileLevel(null); - } + HostMediaRecorder.MimeType mimeType = HostMediaRecorder.MimeType.typeOf(mimeTypeValue); + if (mimeType == HostMediaRecorder.MimeType.UNKNOWN) { + MessageUtils.setInvalidRequestParameterError(response, "mimeType is unknown."); + sendResponse(response); + return; + } - if (profileLevel != null) { - settings.setProfileLevel(profileLevel); - } + encoderSettings = new HostMediaRecorder.EncoderSettings(getContext(), encoderId); + encoderSettings.setName(name); + encoderSettings.setMimeType(mimeType); + encoderSettings.setPreviewSize(new Size(640, 480)); + encoderSettings.setPort(14000); + encoderSettings.setPreviewMaxFrameRate(30); + encoderSettings.setPreviewBitRate(2 * 1024 * 1024); + encoderSettings.setPreviewKeyFrameInterval(5); + encoderSettings.setPreviewQuality(80); + encoderSettings.setBroadcastURI("rtmp://localhost:1935"); + encoderSettings.setRetryCount(0); + encoderSettings.setRetryInterval(3000); + + ((AbstractMediaRecorder) recorder).addEncoder(encoderId, encoderSettings); + } + } - if (previewIntraRefresh != null) { - settings.setIntraRefresh(previewIntraRefresh); - } + if (width != null && height != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setPreviewSize(new Size(width, height)); + } + isChangeConfig = true; + } - if (previewJpegQuality != null) { - settings.setPreviewQuality((int) (previewJpegQuality * 100)); - } + if (frameRate != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setPreviewMaxFrameRate(frameRate); + } + isChangeConfig = true; + } - if (previewClipReset != null && previewClipReset) { - settings.setDrawingRange(null); - } else if (previewClipLeft != null) { - settings.setDrawingRange(new Rect(previewClipLeft, previewClipTop, previewClipRight, previewClipBottom)); - } + if (bitRate != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setPreviewBitRate(bitRate * 1024); + } - try { - recorder.onConfigChange(); - } catch (Exception e) { - MessageUtils.setIllegalDeviceStateError(response, "Failed to change a config."); - return; - } + recorder.requestBitRate(); + } - setResult(response, DConnectMessage.RESULT_OK); - } + if (keyFrameInterval != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setPreviewKeyFrameInterval(keyFrameInterval); + } + isChangeConfig = true; + } - @Override - public String getAttribute() { - return ATTRIBUTE_OPTIONS; - } + if (codec != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setPreviewEncoder(codec); + settings.getEncoderSetting(encoderId).setProfileLevel(null); + } + isChangeConfig = true; + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - final String target = getTarget(request); + if (profileLevel != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setProfileLevel(profileLevel); + } + isChangeConfig = true; + } - final HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (intraRefresh != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setIntraRefresh(intraRefresh); + } + isChangeConfig = true; + } - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; - } + if (useSoftwareEncoder != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setUseSoftwareEncoder(useSoftwareEncoder); + } + isChangeConfig = true; + } - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - setOptions(request, response); - sendResponse(response); - } + if (jpegQuality != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setPreviewQuality((int) (jpegQuality * 100)); + } + recorder.requestJpegQuality(); + } - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); - } - }); - return false; - } - }; + if (broadcastUri != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setBroadcastURI(broadcastUri); + } + isChangeConfig = true; + } - // POST /gotapi/mediaStreamRecording/preview/requestKeyFrame - private final DConnectApi mPostPreviewRequestKeyFrameApi = new PostApi() { + if (retryCount != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setRetryCount(retryCount); + } + } - @Override - public String getInterface() { - return "preview"; - } + if (retryInterval != null) { + for (String encoderId : encoderIdList) { + settings.getEncoderSetting(encoderId).setRetryInterval(retryInterval); + } + } - @Override - public String getAttribute() { - return "requestKeyFrame"; - } + try { + if (isChangeConfig) { + recorder.onConfigChange(); + } + setResult(response, DConnectMessage.RESULT_OK); + } catch (Exception e) { + MessageUtils.setIllegalDeviceStateError(response, "Failed to change a config."); + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - final String target = getTarget(request); + sendResponse(response); + } - final HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; + return false; } + }); - if (!recorder.isBroadcasterRunning() && !recorder.isPreviewRunning()) { - MessageUtils.setIllegalServerStateError(response, "Recorder has not started previewing."); - } else { - recorder.requestKeyFrame(); - setResult(response, DConnectMessage.RESULT_OK); + // DELETE /gotapi/mediaStreamRecording/encoder + addApi(new DeleteApi() { + @Override + public String getAttribute() { + return "encoder"; } - return true; - } - }; - - // PUT /gotapi/mediaStreamRecording/onPhoto - private final DConnectApi mPutOnPhotoApi = new PutApi() { + @Override + public boolean onRequest(Intent request, Intent response) { + String target = getTarget(request); + String name = request.getStringExtra("name"); - @Override - public String getAttribute() { - return ATTRIBUTE_ON_PHOTO; - } + if (name == null) { + MessageUtils.setInvalidRequestParameterError(response, "name is not set."); + return true; + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - EventError error = EventManager.INSTANCE.addEvent(request); - if (error == EventError.NONE) { - setResult(response, DConnectMessage.RESULT_OK); - } else { - setResult(response, DConnectMessage.RESULT_ERROR); - } - return true; - } - }; + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } - // DELETE /gotapi/mediaStreamRecording/onPhoto - private final DConnectApi mDeleteOnPhotoApi = new DeleteApi() { + String encoderId = recorder.getId() + "-" + name; - @Override - public String getAttribute() { - return ATTRIBUTE_ON_PHOTO; - } + ((AbstractMediaRecorder) recorder).removeEncoder(encoderId); - @Override - public boolean onRequest(final Intent request, final Intent response) { - EventError error = EventManager.INSTANCE.removeEvent(request); - if (error == EventError.NONE) { setResult(response, DConnectMessage.RESULT_OK); - } else { - setResult(response, DConnectMessage.RESULT_ERROR); + return true; } - return true; - } - }; + }); - // PUT /gotapi/mediaStreamRecording/onRecordingChange - private final DConnectApi mPutOnRecordingChangeApi = new PutApi() { + // PUT /gotapi/mediaStreamRecording/crop + addApi(new PutApi() { + @Override + public String getAttribute() { + return "crop"; + } - @Override - public String getAttribute() { - return ATTRIBUTE_ON_RECORDING_CHANGE; - } + @Override + public boolean onRequest(Intent request, Intent response) { + String target = getTarget(request); + String name = request.getStringExtra("name"); + Integer left = parseInteger(request, "left"); + Integer top = parseInteger(request, "top"); + Integer right = parseInteger(request, "right"); + Integer bottom = parseInteger(request, "bottom"); + Integer duration = parseInteger(request, "duration"); - @Override - public boolean onRequest(final Intent request, final Intent response) { - EventError error = EventManager.INSTANCE.addEvent(request); - if (error == EventError.NONE) { - setResult(response, DConnectMessage.RESULT_OK); - } else { - setResult(response, DConnectMessage.RESULT_ERROR); - } - return true; - } - }; + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } - // DELETE /gotapi/mediaStreamRecording/onRecordingChange - private final DConnectApi mDeleteOnRecordingChangeApi = new DeleteApi() { + if (left == null) { + MessageUtils.setInvalidRequestParameterError(response, "left is not set."); + return true; + } - @Override - public String getAttribute() { - return ATTRIBUTE_ON_RECORDING_CHANGE; - } + if (top == null) { + MessageUtils.setInvalidRequestParameterError(response, "top is not set."); + return true; + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - EventError error = EventManager.INSTANCE.removeEvent(request); - if (error == EventError.NONE) { - setResult(response, DConnectMessage.RESULT_OK); - } else { - setResult(response, DConnectMessage.RESULT_ERROR); - } - return true; - } - }; + if (right == null) { + MessageUtils.setInvalidRequestParameterError(response, "right is not set."); + return true; + } - // POST /gotapi/mediaStreamRecording/takePhoto - private final DConnectApi mPostTakePhotoApi = new PostApi() { + if (bottom == null) { + MessageUtils.setInvalidRequestParameterError(response, "bottom is not set."); + return true; + } - @Override - public String getAttribute() { - return ATTRIBUTE_TAKE_PHOTO; - } + List encoderIdList = getEncoderSettings(recorder, name, true); + if (encoderIdList.isEmpty()) { + MessageUtils.setInvalidRequestParameterError(response, "name is invalid."); + return true; + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - final String target = getTarget(request); - final String serviceId = getServiceID(request); + if (left >= right || top >= bottom) { + MessageUtils.setInvalidRequestParameterError(response, "parameter is invalid."); + return true; + } + + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + for (String encoderId : encoderIdList) { + setCrop(recorder, encoderId, left, top, right, bottom, duration); + } + setResult(response, DConnectMessage.RESULT_OK); + sendResponse(response); + } - final HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; + return false; } + }); - // TODO 他のカメラとは排他的に処理を行うようにします。 - if (!mRecorderMgr.canUseRecorder(recorder)) { - // 他のカメラが使用中の場合はエラーを返却 - MessageUtils.setIllegalDeviceStateError(response, "Other cameras are being used."); - return true; + // DELETE /gotapi/mediaStreamRecording/crop + addApi(new DeleteApi() { + @Override + public String getAttribute() { + return "crop"; } - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - // TODO 他のカメラとは排他的に処理を行うようにします。 - if (recorder instanceof Camera2Recorder) { - // 使用する予定のレコーダがカメラの場合は、使用していない他のカメラを停止する - mRecorderMgr.stopCameraRecorder(recorder); - } - - recorder.takePhoto(new HostDevicePhotoRecorder.OnPhotoEventListener() { - @Override - public void onTakePhoto(final String uri, final String filePath, final String mimeType) { - setResult(response, DConnectMessage.RESULT_OK); - setUri(response, uri); - setPath(response, filePath); - sendResponse(response); - } + @Override + public boolean onRequest(Intent request, Intent response) { + String target = getTarget(request); + String name = request.getStringExtra("name"); - @Override - public void onFailedTakePhoto(final String errorMessage) { - MessageUtils.setUnknownError(response, errorMessage); - sendResponse(response); - } - }); + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; } - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); + List encoderIdList = getEncoderSettings(recorder, name, true); + if (encoderIdList.isEmpty()) { + MessageUtils.setInvalidRequestParameterError(response, "name is invalid."); + return true; } - }); - - return false; - } - }; - - // PUT /gotapi/mediaStreamRecording/preview - private final DConnectApi mPutPreviewApi = new PutApi() { - - @Override - public String getAttribute() { - return ATTRIBUTE_PREVIEW; - } - @Override - public boolean onRequest(final Intent request, final Intent response) { - String target = getTarget(request); + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + for (String encoderId : encoderIdList) { + clearCrop(recorder, encoderId); + } + setResult(response, DConnectMessage.RESULT_OK); + sendResponse(response); + } - HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; + return false; } + }); - // TODO 他のカメラとは排他的に処理を行うようにします。 - if (!mRecorderMgr.canUseRecorder(recorder)) { - // 他のカメラが使用中の場合はエラーを返却 - MessageUtils.setIllegalDeviceStateError(response, "Other cameras are being used."); - return true; + // PUT /gotapi/mediaStreamRecording/broadcast + addApi(new PutApi() { + @Override + public String getAttribute() { + return "broadcast"; } - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - HostDevicePlugin plugin = getHostDevicePlugin(); - plugin.getSSLContext(sslContext -> { - recorder.setSSLContext(sslContext); + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); + String broadcastURI = request.getStringExtra("broadcastURI"); - List servers = recorder.startPreview(); - if (servers.isEmpty()) { - MessageUtils.setIllegalServerStateError(response, "Failed to start web server."); - } else { - String defaultUri = null; - List streams = new ArrayList<>(); - for (PreviewServer server : servers) { - // Motion-JPEG をデフォルトの値として使用します - if (defaultUri == null && server.getUri().startsWith("http://") - && "video/x-mjpeg".equals(server.getMimeType())) { - defaultUri = server.getUri(); - } + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); - Bundle stream = new Bundle(); - stream.putString("mimeType", server.getMimeType()); - stream.putString("uri", server.getUri()); - streams.add(stream); - } - setResult(response, DConnectMessage.RESULT_OK); - setUri(response, defaultUri != null ? defaultUri : ""); - response.putExtra("streams", streams.toArray(new Bundle[0])); - } + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } - sendResponse(response); - }); + if (broadcastURI == null) { + MessageUtils.setInvalidRequestParameterError(response, "broadcastURI is not set."); + return true; } - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); + if (recorder.isBroadcasterRunning()) { + MessageUtils.setIllegalServerStateError(response, "broadcastURI is already running."); + return true; } - }); - return false; - } - }; + // TODO 他のカメラとは排他的に処理を行うようにします。 + if (!mRecorderMgr.canUseRecorder(recorder)) { + // 他のカメラが使用中の場合はエラーを返却 + MessageUtils.setIllegalDeviceStateError(response, "Other cameras are being used."); + return true; + } - // DELETE /gotapi/mediaStreamRecording/preview - private final DConnectApi mDeletePreviewApi = new DeleteApi() { + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + // TODO 他のカメラとは排他的に処理を行うようにします。 + if (recorder instanceof Camera2Recorder) { + // 使用する予定のレコーダがカメラの場合は、使用していない他のカメラを停止する + mRecorderMgr.stopCameraRecorder(recorder); + } - @Override - public String getAttribute() { - return ATTRIBUTE_PREVIEW; - } + for (String encoderId : recorder.getSettings().getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = recorder.getSettings().getEncoderSetting(encoderId); + if (encoderSettings != null && encoderSettings.getMimeType() == HostMediaRecorder.MimeType.RTMP) { + encoderSettings.setBroadcastURI(broadcastURI); + } + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - String target = getTarget(request); + List broadcasters = recorder.startBroadcaster(broadcastURI); + if (!broadcasters.isEmpty()) { + setResult(response, DConnectMessage.RESULT_OK); + } else { + MessageUtils.setIllegalServerStateError(response, "Failed to start a broadcast."); + } + sendResponse(response); + } - final HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); + return false; + } + }); - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; + // DELETE /gotapi/mediaStreamRecording/broadcast + addApi(new DeleteApi() { + + @Override + public String getAttribute() { + return "broadcast"; } - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - recorder.stopPreview(); - setResult(response, DConnectMessage.RESULT_OK); - sendResponse(response); - } - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); - } - }); - return false; - } - }; + @Override + public boolean onRequest(final Intent request, final Intent response) { + String target = getTarget(request); - // PUT /gotapi/mediaStreamRecording/preview/mute - private final DConnectApi mPutPreviewMuteApi = new PutApi() { + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); - @Override - public String getInterface() { return ATTRIBUTE_PREVIEW; } - @Override - public String getAttribute() { - return "mute"; - } + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return true; + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - return setMute(true, request, response); - } - }; + recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { + @Override + public void onAllowed() { + recorder.stopBroadcaster(); + setResult(response, DConnectMessage.RESULT_OK); + sendResponse(response); + } - // DELETE /gotapi/mediaStreamRecording/preview/mute - private final DConnectApi mDeletePreviewMuteApi = new DeleteApi() { + @Override + public void onDisallowed() { + MessageUtils.setUnknownError(response, "Permission for camera is not granted."); + sendResponse(response); + } + }); + return false; + } + }); + } - @Override - public String getInterface() { return ATTRIBUTE_PREVIEW; } - @Override - public String getAttribute() { - return "mute"; - } + private HostDevicePlugin getHostDevicePlugin() { + return (HostDevicePlugin) getContext(); + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - return setMute(false, request, response); + private List getEncoderSettings(HostMediaRecorder recorder, String name, boolean checkExist) { + List encoderIdList = new ArrayList<>(); + if (name == null) { + encoderIdList.addAll(recorder.getSettings().getEncoderIdList()); + } else { + String[] names = name.split(","); + for (String n : names) { + String encoderId = recorder.getId() + "-" + n; + if (!checkExist || recorder.getSettings().existEncoderId(encoderId)) { + encoderIdList.add(encoderId); + } + } } - }; + return encoderIdList; + } /** * RecorderのMute状態を切り返す. @@ -841,413 +1336,542 @@ public void onDisallowed() { return false; } - // POST /gotapi/mediaStreamRecording/record - private final DConnectApi mPostRecordApi = new PostApi() { + private void setOptions(final Intent request, final Intent response) { + String target = getTarget(request); + String mimeType = getMIMEType(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"); + Integer previewClipDuration = parseInteger(request, "previewClipDuration"); + Boolean previewClipReset = parseBoolean(request, "previewClipReset"); + + HostMediaRecorder.ProfileLevel profileLevel = null; + Range fps = null; + boolean isChangeConfig = false; - @Override - public String getAttribute() { - return ATTRIBUTE_RECORD; + HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (recorder == null) { + MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); + return; } - @Override - public boolean onRequest(final Intent request, final Intent response) { - String target = getTarget(request); + HostMediaRecorder.Settings settings = recorder.getSettings(); - HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; +// if (!isSupportedMimeType(recorder, mimeType)) { +// MessageUtils.setInvalidRequestParameterError(response, "MIME-Type " + mimeType + " is unsupported."); +// return; +// } + + if (recorder.getState() != HostMediaRecorder.State.INACTIVE + && recorder.getState() != HostMediaRecorder.State.PREVIEW) { + MessageUtils.setInvalidRequestParameterError(response, "settings of active target cannot be changed."); + return; + } + + // 値の妥当性チェック + + if (imageWidth != null || imageHeight != null) { + if (imageWidth == null) { + MessageUtils.setInvalidRequestParameterError(response, "imageWidth is not set."); + return; } - // TODO 他のカメラとは排他的に処理を行うようにします。 - if (!mRecorderMgr.canUseRecorder(recorder)) { - // 他のカメラが使用中の場合はエラーを返却 - MessageUtils.setIllegalDeviceStateError(response, "Other cameras are being used."); - return true; + if (imageHeight == null) { + MessageUtils.setInvalidRequestParameterError(response, "imageHeight is not set."); + return; } - // 撮影がすでに行われている場合はエラーを返却 - if (recorder.getState() != HostMediaRecorder.State.INACTIVE - && recorder.getState() != HostMediaRecorder.State.PREVIEW) { - MessageUtils.setIllegalDeviceStateError(response, - recorder.getName() + " is already running."); - return true; + if (!settings.isSupportedPictureSize(imageWidth, imageHeight)) { + MessageUtils.setInvalidRequestParameterError(response, "Unsupported image size: imageWidth = " + + imageWidth + ", imageHeight = " + imageHeight); + return; } + } - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - recorder.startRecording(new HostDeviceStreamRecorder.RecordingCallback() { - @Override - public void onRecorded(final HostDeviceStreamRecorder streamRecorder, final String fileName) { - setResult(response, DConnectMessage.RESULT_OK); - setPath(response, "/" + fileName); - setUri(response, mFileManager.getContentUri() + "/" + fileName); - sendResponse(response); - } + if (previewWidth != null || previewHeight != null) { + if (previewWidth == null) { + MessageUtils.setInvalidRequestParameterError(response, "previewWidth is not set."); + return; + } - @Override - public void onFailed(final HostDeviceStreamRecorder recorder, final String errorMessage) { - MessageUtils.setIllegalServerStateError(response, errorMessage); - sendResponse(response); - } - }); - } + if (previewHeight == null) { + MessageUtils.setInvalidRequestParameterError(response, "previewHeight is not set."); + return; + } - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); - } - }); - return false; + if (!settings.isSupportedPreviewSize(previewWidth, previewHeight)) { + MessageUtils.setInvalidRequestParameterError(response, "Unsupported preview size: previewWidth = " + + previewWidth + ", previewHeight = " + previewHeight); + return; + } } - }; - // PUT /gotapi/mediaStreamRecording/stop - private final DConnectApi mPutStopApi = new PutApi() { - - @Override - public String getAttribute() { - return ATTRIBUTE_STOP; + if (previewMaxFrameRate != null) { + fps = recorder.getSettings().getPreviewFpsFromFrameRate(previewMaxFrameRate.intValue()); + if (fps == null) { + MessageUtils.setInvalidRequestParameterError(response, "previewMaxFrameRate is not supported."); + } } - @Override - public boolean onRequest(final Intent request, final Intent response) { - String target = getTarget(request); - - HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; + if (previewEncoder != null) { + if (!settings.isSupportedVideoEncoder(previewEncoder)) { + MessageUtils.setInvalidRequestParameterError(response, + "Unsupported preview encoder: " + previewEncoder); + return; } + } - if (recorder.getState() == HostMediaRecorder.State.INACTIVE) { - MessageUtils.setIllegalDeviceStateError(response, "recorder is stopped already."); - return true; + if (previewProfile != null || previewLevel != null) { + if (previewProfile == null) { + MessageUtils.setInvalidRequestParameterError(response, + "previewProfile is not set."); + return; } - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - recorder.stopRecording(new HostDeviceStreamRecorder.StoppingCallback() { - @Override - public void onStopped(HostDeviceStreamRecorder streamRecorder, String fileName) { - setResult(response, DConnectMessage.RESULT_OK); - setPath(response, "/" + fileName); - setUri(response, mFileManager.getContentUri() + "/" + fileName); - sendResponse(response); - } - - @Override - public void onFailed(HostDeviceStreamRecorder recorder, String errorMessage) { - MessageUtils.setIllegalServerStateError(response, errorMessage); - sendResponse(response); - } - }); - } + if (previewLevel == null) { + MessageUtils.setInvalidRequestParameterError(response, + "previewLevel is not set."); + return; + } - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); - } - }); + HostMediaRecorder.VideoCodec codec = HostMediaRecorder.VideoCodec.nameOf(previewEncoder); + switch (codec) { + case H264: { + H264Profile p = H264Profile.nameOf(previewProfile); + H264Level l = H264Level.nameOf(previewLevel); + if (p == null || l == null || !settings.isSupportedProfileLevel(codec, p.getValue(), l.getValue())) { + MessageUtils.setInvalidRequestParameterError(response, + "Unsupported preview profile and level: " + previewProfile + " - " + previewLevel); + return; + } + profileLevel = new HostMediaRecorder.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(codec, p.getValue(), l.getValue())) { + MessageUtils.setInvalidRequestParameterError(response, + "Unsupported preview profile and level: " + previewProfile + " - " + previewLevel); + return; + } + profileLevel = new HostMediaRecorder.ProfileLevel(p.getValue(), l.getValue()); + } break; + } + } - return false; + if (previewJpegQuality != null) { + if (previewJpegQuality < 0.0 || previewJpegQuality > 1.0) { + MessageUtils.setInvalidRequestParameterError(response, + "previewJpegQuality is invalid. value=" + previewJpegQuality); + return; + } } - }; - // PUT /gotapi/mediaStreamRecording/pause - private final DConnectApi mPutPauseApi = new PutApi() { + if (previewClipLeft != null || previewClipTop != null + || previewClipRight != null || previewClipBottom != null) { + if (previewClipLeft == null) { + MessageUtils.setInvalidRequestParameterError(response, "previewClipLeft is not set."); + return; + } - @Override - public String getAttribute() { - return ATTRIBUTE_PAUSE; - } + if (previewClipTop == null) { + MessageUtils.setInvalidRequestParameterError(response, "previewClipTop is not set."); + return; + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - String target = getTarget(request); + if (previewClipRight == null) { + MessageUtils.setInvalidRequestParameterError(response, "previewClipRight is not set."); + return; + } - HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; + if (previewClipBottom == null) { + MessageUtils.setInvalidRequestParameterError(response, "previewClipBottom is not set."); + return; } - if (!recorder.canPauseRecording()) { - MessageUtils.setNotSupportAttributeError(response); - return true; + if (previewClipLeft < 0) { + MessageUtils.setInvalidRequestParameterError(response, + "previewClipLeft cannot set a negative value."); + return; } - if (recorder.getState() != HostMediaRecorder.State.RECORDING) { - MessageUtils.setIllegalDeviceStateError(response); - return true; + if (previewClipBottom < 0) { + MessageUtils.setInvalidRequestParameterError(response, + "previewClipBottom cannot set a negative value."); + return; } - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - recorder.pauseRecording(); + if (previewClipLeft >= previewClipRight) { + MessageUtils.setInvalidRequestParameterError(response, + "previewClipLeft is larger than previewClipRight."); + return; + } - setResult(response, DConnectMessage.RESULT_OK); - sendResponse(response); - } + if (previewClipTop >= previewClipBottom) { + MessageUtils.setInvalidRequestParameterError(response, + "previewClipTop is larger than previewClipBottom."); + return; + } - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); + if (previewClipDuration != null) { + if (previewClipDuration < 0) { + MessageUtils.setInvalidRequestParameterError(response, + "previewClipDuration is negative value."); + return; } - }); + } else { + previewClipDuration = 0; + } + } + + // 値の設定 - return false; + if (imageWidth != null && imageHeight != null) { + settings.setPictureSize(new Size(imageWidth, imageHeight)); } - }; - // PUT /gotapi/mediaStreamRecording/resume - private final DConnectApi mPutResumeApi = new PutApi() { + if (previewWidth != null && previewHeight != null) { + settings.setPreviewSize(new Size(previewWidth, previewHeight)); + + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(encoderId); + if (encoderId != null && mimeType.equals(encoderSettings.getMimeType().getValue())) { + encoderSettings.setPreviewSize(new Size(previewWidth, previewHeight)); + } + } - @Override - public String getAttribute() { - return ATTRIBUTE_RESUME; + isChangeConfig = true; } - @Override - public boolean onRequest(final Intent request, final Intent response) { - String target = getTarget(request); + if (fps != null) { + settings.setPreviewFps(fps); - HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(encoderId); + if (encoderId != null && mimeType.equals(encoderSettings.getMimeType().getValue())) { + encoderSettings.setPreviewMaxFrameRate(previewMaxFrameRate.intValue()); + } } - if (!recorder.canPauseRecording()) { - MessageUtils.setNotSupportAttributeError(response); - return true; - } + isChangeConfig = true; + } - if (recorder.getState() != HostMediaRecorder.State.PAUSED) { - MessageUtils.setIllegalDeviceStateError(response); - return true; + if (previewBitRate != null) { + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(encoderId); + if (encoderId != null && mimeType.equals(encoderSettings.getMimeType().getValue())) { + encoderSettings.setPreviewBitRate(previewBitRate * 1024); + } } - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - recorder.resumeRecording(); - setResult(response, DConnectMessage.RESULT_OK); - sendResponse(response); - } + recorder.requestBitRate(); + } - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); + if (previewKeyFrameInterval != null) { + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(encoderId); + if (encoderId != null && mimeType.equals(encoderSettings.getMimeType().getValue())) { + encoderSettings.setPreviewKeyFrameInterval(previewKeyFrameInterval); } - }); + } - return false; + isChangeConfig = true; } - }; - private final HostMediaRecorderManager.OnEventListener mOnEventListener = new HostMediaRecorderManager.OnEventListener() { - @Override - public void onMuteChanged(HostMediaRecorder recorder, boolean mute) { - } + if (previewEncoder != null) { + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(encoderId); + if (encoderId != null && mimeType.equals(encoderSettings.getMimeType().getValue())) { + encoderSettings.setPreviewEncoder(previewEncoder); + encoderSettings.setProfileLevel(null); + } + } - @Override - public void onConfigChanged(HostMediaRecorder recorder) { + isChangeConfig = true; } - @Override - public void onPreviewStarted(HostMediaRecorder recorder, List servers) { - } + if (profileLevel != null) { + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(encoderId); + if (encoderId != null && mimeType.equals(encoderSettings.getMimeType().getValue())) { + encoderSettings.setProfileLevel(profileLevel); + } + } - @Override - public void onPreviewStopped(HostMediaRecorder recorder) { + isChangeConfig = true; } - @Override - public void onPreviewError(HostMediaRecorder recorder, Exception e) { - } + if (previewIntraRefresh != null) { + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(encoderId); + if (encoderId != null && mimeType.equals(encoderSettings.getMimeType().getValue())) { + encoderSettings.setIntraRefresh(previewIntraRefresh); + } + } - @Override - public void onBroadcasterStarted(HostMediaRecorder recorder, Broadcaster broadcaster) { + isChangeConfig = true; } - @Override - public void onBroadcasterStopped(HostMediaRecorder recorder, Broadcaster broadcaster) { + if (previewJpegQuality != null) { + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(encoderId); + if (encoderId != null && mimeType.equals(encoderSettings.getMimeType().getValue())) { + encoderSettings.setPreviewQuality((int) (previewJpegQuality * 100)); + } + } + recorder.requestJpegQuality(); } - @Override - public void onBroadcasterError(HostMediaRecorder recorder, Broadcaster broadcaster, Exception e) { + if (previewClipReset != null && previewClipReset) { + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(encoderId); + if (encoderId != null && mimeType.equals(encoderSettings.getMimeType().getValue())) { + clearCrop(recorder, encoderId); + } + } + } else if (previewClipLeft != null) { + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSettings = settings.getEncoderSetting(encoderId); + if (encoderId != null && mimeType.equals(encoderSettings.getMimeType().getValue())) { + setCrop(recorder, encoderId, previewClipLeft, previewClipTop, + previewClipRight, previewClipBottom, previewClipDuration); + } + } } - @Override - public void onTakePhoto(HostMediaRecorder recorder, String uri, String filePath, String mimeType) { - sendEventForTakePhoto(getService().getId(), uri, filePath, mimeType); + if (isChangeConfig) { + try { + recorder.onConfigChange(); + } catch (Exception e) { + MessageUtils.setIllegalDeviceStateError(response, "Failed to change a config."); + return; + } } - @Override - public void onRecordingStarted(HostMediaRecorder recorder, String fileName) { - sendEventForRecordingChange(getService().getId(), recorder.getState(), - mFileManager.getContentUri() + "/" + fileName, - "/" + fileName, recorder.getMimeType(), null); - } + setResult(response, DConnectMessage.RESULT_OK); + } - @Override - public void onRecordingPause(HostMediaRecorder recorder) { + private void setCrop(HostMediaRecorder recorder, String encoderId, int left, int top, int right, int bottom, int duration) { + HostMediaRecorder.EncoderSettings encoderSettings = recorder.getSettings().getEncoderSetting(encoderId); + if (encoderSettings == null) { + return; } - @Override - public void onRecordingResume(HostMediaRecorder recorder) { + Rect start = encoderSettings.getCropRect(); + if (start == null) { + int width = recorder.getSettings().getPreviewSize().getWidth(); + int height = recorder.getSettings().getPreviewSize().getHeight(); + start = new Rect(0, 0, width, height); } - @Override - public void onRecordingStopped(HostMediaRecorder recorder, String fileName) { - sendEventForRecordingChange(getService().getId(), recorder.getState(), - mFileManager.getContentUri() + "/" + fileName, - "/" + fileName, recorder.getMimeType(), null); - } + Rect end = new Rect(left, top, right, bottom); - @Override - public void onError(HostMediaRecorder recorder, Exception e) { + for (LiveStreaming previewServer : recorder.getServerProvider().getLiveStreamingList()) { + if (encoderId.equals(previewServer.getId())) { + ((CropInterface) previewServer).moveCropRect(start, end, duration); + } } - }; - - public HostMediaStreamingRecordingProfile(final HostMediaRecorderManager mgr, final FileManager fileMgr) { - mFileManager = fileMgr; - mRecorderMgr = mgr; - mRecorderMgr.addOnEventListener(mOnEventListener); - - addApi(mGetMediaRecorderApi); - - addApi(mGetOptionsApi); - addApi(mPutOptionsApi); - - addApi(mPutOnPhotoApi); - addApi(mDeleteOnPhotoApi); - addApi(mPostTakePhotoApi); - addApi(mPostRecordApi); - addApi(mPutStopApi); - addApi(mPutPauseApi); - addApi(mPutResumeApi); - addApi(mPutOnRecordingChangeApi); - addApi(mDeleteOnRecordingChangeApi); - addApi(mPutPreviewApi); - addApi(mDeletePreviewApi); - addApi(mPostPreviewRequestKeyFrameApi); - addApi(mPutPreviewMuteApi); - addApi(mDeletePreviewMuteApi); - - // PUT /gotapi/mediaStreamRecording/broadcast - addApi(new PutApi() { - @Override - public String getAttribute() { - return "broadcast"; + for (LiveStreaming broadcaster : recorder.getBroadcasterProvider().getLiveStreamingList()) { + if (encoderId.equals(broadcaster.getId())) { + ((CropInterface) broadcaster).moveCropRect(start, end, duration); } + } + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - String target = getTarget(request); - String broadcastURI = request.getStringExtra("broadcastURI"); + private void clearCrop(HostMediaRecorder recorder, String encoderId) { + for (LiveStreaming previewServer : recorder.getServerProvider().getLiveStreamingList()) { + if (encoderId.equals(previewServer.getId())) { + ((CropInterface) previewServer).setCropRect(null); + } + } - HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + for (LiveStreaming broadcaster : recorder.getBroadcasterProvider().getLiveStreamingList()) { + if (encoderId.equals(broadcaster.getId())) { + ((CropInterface) broadcaster).setCropRect(null); + } + } + } - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; + private HostMediaRecorder.ProfileLevel convertProfileLevel(HostMediaRecorder recorder, String codec, String profile, String level) { + if (profile != null && level != null) { + HostMediaRecorder.VideoCodec codec1 = HostMediaRecorder.VideoCodec.H264; + if (codec != null) { + codec1 = HostMediaRecorder.VideoCodec.nameOf(codec); + } + switch (codec1) { + case H264: { + H264Profile p = H264Profile.nameOf(profile); + H264Level l = H264Level.nameOf(level); + if (p == null || l == null || !recorder.getSettings().isSupportedProfileLevel(codec1, p.getValue(), l.getValue())) { + return null; + } + return new HostMediaRecorder.ProfileLevel(p.getValue(), l.getValue()); } - - if (broadcastURI == null) { - MessageUtils.setInvalidRequestParameterError(response, "broadcastURI is not set."); - return true; + case H265: { + H265Profile p = H265Profile.nameOf(profile); + H265Level l = H265Level.nameOf(level); + if (p == null || l == null || !recorder.getSettings().isSupportedProfileLevel(codec1, p.getValue(), l.getValue())) { + return null; + } + return new HostMediaRecorder.ProfileLevel(p.getValue(), l.getValue()); } + } + } + return null; + } - if (recorder.isBroadcasterRunning()) { - MessageUtils.setIllegalServerStateError(response, "broadcastURI is already running."); - return true; - } + private Bundle createMediaRecorderInfo(HostMediaRecorder recorder) { + HostMediaRecorder.Settings settings = recorder.getSettings(); - // TODO 他のカメラとは排他的に処理を行うようにします。 - if (!mRecorderMgr.canUseRecorder(recorder)) { - // 他のカメラが使用中の場合はエラーを返却 - MessageUtils.setIllegalDeviceStateError(response, "Other cameras are being used."); - return true; - } + Bundle info = new Bundle(); + setRecorderId(info, recorder.getId()); + setRecorderName(info, recorder.getName()); + setRecorderMIMEType(info, recorder.getMimeType()); - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - // TODO 他のカメラとは排他的に処理を行うようにします。 - if (recorder instanceof Camera2Recorder) { - // 使用する予定のレコーダがカメラの場合は、使用していない他のカメラを停止する - mRecorderMgr.stopCameraRecorder(recorder); - } + if (recorder.getState() == HostMediaRecorder.State.RECORDING) { + setRecorderState(info, RecorderState.RECORDING); + } else { + setRecorderState(info, RecorderState.INACTIVE); + } - Broadcaster b = recorder.startBroadcaster(broadcastURI); - if (b != null) { - setResult(response, DConnectMessage.RESULT_OK); - } else { - MessageUtils.setIllegalServerStateError(response, "Failed to start a broadcast."); - } - sendResponse(response); - } + // 静止画の解像度 + Size pictureSize = settings.getPictureSize(); + if (pictureSize != null) { + setRecorderImageWidth(info, pictureSize.getWidth()); + setRecorderImageHeight(info, pictureSize.getHeight()); + } - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); - } - }); - return false; - } - }); + // プレビュー解像度 + Size previewSize = settings.getPreviewSize(); + if (previewSize != null) { + setRecorderPreviewWidth(info, previewSize.getWidth()); + setRecorderPreviewHeight(info, previewSize.getHeight()); + } - // DELETE /gotapi/mediaStreamRecording/broadcast - addApi(new DeleteApi() { + // カメラのフレームレート + Range previewFps = settings.getPreviewFps(); + if (previewFps != null) { + info.putString("previewFps", previewFps.getLower() + "-" + previewFps.getUpper()); + info.putInt("previewMaxFrameRate", previewFps.getUpper()); + } - @Override - public String getAttribute() { - return "broadcast"; + // エンコーダ設定 + List encoders = new ArrayList<>(); + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings s = settings.getEncoderSetting(encoderId); + if (s != null) { + encoders.add(createVideoEncoder(s)); } + } + info.putParcelableArray("encoders", encoders.toArray(new Bundle[0])); + + // 各機能の状態 + Bundle status = new Bundle(); + status.putBoolean("preview", recorder.isPreviewRunning()); + status.putBoolean("broadcast", recorder.isBroadcasterRunning()); + status.putBoolean("recording", recorder.getState() == HostMediaRecorder.State.RECORDING); + info.putParcelable("status", status); + + info.putString("audioSource", settings.getPreviewAudioSource().getValue()); + info.putInt("audioBitrate", settings.getPreviewAudioBitRate() / 1024); + info.putInt("audioSampleRate", settings.getPreviewSampleRate()); + info.putInt("audioChannel", settings.getPreviewChannel()); + info.putBoolean("audioEchoCanceler", settings.isUseAEC()); + + setRecorderConfig(info, ""); + return info; + } - @Override - public boolean onRequest(final Intent request, final Intent response) { - String target = getTarget(request); + private Bundle createVideoEncoder(HostMediaRecorder.EncoderSettings s) { + Bundle bundle = new Bundle(); + bundle.putString("name", s.getName()); + bundle.putString("mimeType", s.getMimeType().getValue()); + bundle.putInt("width", s.getPreviewSize().getWidth()); + bundle.putInt("height", s.getPreviewSize().getHeight()); + bundle.putInt("bitRate", s.getPreviewBitRate() / 1024); + + if ("video/x-mjpeg".equals(s.getMimeType().getValue())) { + bundle.putFloat("jpegQuality", s.getPreviewQuality() / 100.0f); + } else if (s.getMimeType().getValue().startsWith("video/")) { + bundle.putInt("keyFrameInterval", s.getPreviewKeyFrameInterval()); + bundle.putString("encoder", s.getPreviewEncoder()); + HostMediaRecorder.ProfileLevel pl = s.getProfileLevel(); + if (pl != null) { + switch (HostMediaRecorder.VideoCodec.nameOf(s.getPreviewEncoder())) { + case H264: + bundle.putString("previewProfile", H264Profile.valueOf(pl.getProfile()).getName()); + bundle.putString("previewLevel", H264Level.valueOf(pl.getLevel()).getName()); + break; + case H265: + bundle.putString("previewProfile", H265Profile.valueOf(pl.getProfile()).getName()); + bundle.putString("previewLevel", H265Level.valueOf(pl.getLevel()).getName()); + break; + } + } + bundle.putBoolean("useSoftwareEncoder", s.isUseSoftwareEncoder()); + Integer intraRefresh = s.getIntraRefresh(); + if (intraRefresh != null) { + bundle.putInt("intraRefresh", intraRefresh); + } + } - HostMediaRecorder recorder = mRecorderMgr.getRecorder(target); + if (s.getPort() > 0) { + bundle.putInt("port", s.getPort()); + } - if (recorder == null) { - MessageUtils.setInvalidRequestParameterError(response, "target is invalid."); - return true; - } + // 切り抜き設定 + Rect rect = s.getCropRect(); + 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); + bundle.putBundle("crop", drawingRect); + } - recorder.requestPermission(new HostMediaRecorder.PermissionCallback() { - @Override - public void onAllowed() { - recorder.stopBroadcaster(); - setResult(response, DConnectMessage.RESULT_OK); - sendResponse(response); - } + return bundle; + } - @Override - public void onDisallowed() { - MessageUtils.setUnknownError(response, "Permission for camera is not granted."); - sendResponse(response); - } - }); - return false; - } - }); + private Bundle createRecorderOption(HostMediaRecorder recorder) { + Bundle bundle = new Bundle(); + HostMediaRecorder.Settings settings = recorder.getSettings(); + setRecorderId(bundle, recorder.getId()); + setRecorderName(bundle, recorder.getName()); + if (recorder.getMimeType().startsWith("image/") || recorder.getMimeType().startsWith("video/")) { + setSupportedImageSizes(bundle, settings.getSupportedPictureSizes()); + setSupportedPreviewSizes(bundle, settings.getSupportedPreviewSizes()); + setSupportedVideoEncoders(bundle, settings.getSupportedVideoEncoders()); + setSupportedFps(bundle, settings.getSupportedFps()); + } + setMIMEType(bundle, recorder.getSupportedMimeTypes()); + return bundle; } - private HostDevicePlugin getHostDevicePlugin() { - return (HostDevicePlugin) getContext(); + private static Bundle[] createSupportedImageSizes(List sizes) { + Bundle[] array = new Bundle[sizes.size()]; + int i = 0; + for (Size size : sizes) { + Bundle info = new Bundle(); + setWidth(info, size.getWidth()); + setHeight(info, size.getHeight()); + array[i++] = info; + } + return array; } /** @@ -1257,6 +1881,20 @@ private HostDevicePlugin getHostDevicePlugin() { * @param sizes サポートしている静止画の解像度のリスト */ private static void setSupportedImageSizes(final Intent response, final List sizes) { + setImageSizes(response, createSupportedImageSizes(sizes)); + } + + /** + * サポートしている静止画の解像度をレスポンスに格納します. + * + * @param bundle 静止画の解像度を格納するレスポンス + * @param sizes サポートしている静止画の解像度のリスト + */ + private static void setSupportedImageSizes(final Bundle bundle, final List sizes) { + bundle.putParcelableArray("imageSizes", createSupportedImageSizes(sizes)); + } + + private static Bundle[] createSupportedPreviewSizes(List sizes) { Bundle[] array = new Bundle[sizes.size()]; int i = 0; for (Size size : sizes) { @@ -1265,7 +1903,7 @@ private static void setSupportedImageSizes(final Intent response, final List sizes) { - Bundle[] array = new Bundle[sizes.size()]; - int i = 0; - for (Size size : sizes) { - Bundle info = new Bundle(); - setWidth(info, size.getWidth()); - setHeight(info, size.getHeight()); - array[i++] = info; - } - setPreviewSizes(response, array); + setPreviewSizes(response, createSupportedPreviewSizes(sizes)); } /** - * サポートしているエンコーダをレスポンスに格納します. + * サポートしているプレビューの解像度をレスポンスに格納します. * - * @param response レスポンス - * @param encoderNames エンコーダのリスト + * @param bundle プレビューの解像度を格納するレスポンス + * @param sizes サポートしているプレビューの解像度のリスト */ - private static void setSupportedVideoEncoders(Intent response, List encoderNames) { + private static void setSupportedPreviewSizes(final Bundle bundle, final List sizes) { + bundle.putParcelableArray("previewSizes", createSupportedPreviewSizes(sizes)); + } + + private static Bundle[] createSupportedVideoEncoders(List encoderNames) { List encoders = new ArrayList<>(); for (String name : encoderNames) { - HostMediaRecorder.VideoEncoderName encoderName = HostMediaRecorder.VideoEncoderName.nameOf(name); + HostMediaRecorder.VideoCodec videoCodec = HostMediaRecorder.VideoCodec.nameOf(name); + Size maxSize = CapabilityUtil.getSupportedMaxSize(videoCodec.getMimeType()); Bundle encoder = new Bundle(); encoder.putString("name", name); - encoder.putParcelableArray("profileLevel", getProfileLevels(encoderName)); + encoder.putParcelableArray("profileLevel", getProfileLevels(videoCodec)); + if (maxSize != null) { + encoder.putInt("maxWidth", maxSize.getWidth()); + encoder.putInt("maxHeight", maxSize.getHeight()); + } encoders.add(encoder); } - response.putExtra("encoder", encoders.toArray(new Bundle[0])); + return encoders.toArray(new Bundle[0]); + } + + /** + * サポートしているエンコーダをレスポンスに格納します. + * + * @param response レスポンス + * @param encoderNames エンコーダのリスト + */ + private static void setSupportedVideoEncoders(Intent response, List encoderNames) { + response.putExtra("encoder", createSupportedVideoEncoders(encoderNames)); + } + + /** + * サポートしているエンコーダをレスポンスに格納します. + * + * @param bundle レスポンス + * @param encoderNames エンコーダのリスト + */ + private static void setSupportedVideoEncoders(Bundle bundle, List encoderNames) { + bundle.putParcelableArray("encoder", createSupportedVideoEncoders(encoderNames)); } /** * エンコーダがサポートしているプロファイルとレベルを格納した Bundle の配列を取得します. * - * @param encoderName エンコーダ + * @param videoCodec エンコーダ * @return プロファイルとレベルを格納した Bundle の配列 */ - private static Bundle[] getProfileLevels(HostMediaRecorder.VideoEncoderName encoderName) { + private static Bundle[] getProfileLevels(HostMediaRecorder.VideoCodec videoCodec) { List list = new ArrayList<>(); - for (HostMediaRecorder.ProfileLevel pl : CapabilityUtil.getSupportedProfileLevel(encoderName.getMimeType())) { - switch (encoderName) { + for (HostMediaRecorder.ProfileLevel pl : CapabilityUtil.getSupportedProfileLevel(videoCodec.getMimeType())) { + switch (videoCodec) { case H264: { H264Profile p = H264Profile.valueOf(pl.getProfile()); H264Level l = H264Level.valueOf(pl.getLevel()); @@ -1339,6 +1998,14 @@ private static Bundle[] getProfileLevels(HostMediaRecorder.VideoEncoderName enco return list.toArray(new Bundle[0]); } + private static String[] createSupportedFps(List> supportedFps) { + List fpsList = new ArrayList<>(); + for (Range fps : supportedFps) { + fpsList.add(fps.getLower() + "-" + fps.getUpper()); + } + return fpsList.toArray(new String[0]); + } + /** * カメラがサポートしている Fps のリストをレスポンスに格納します. * @@ -1346,13 +2013,17 @@ private static Bundle[] getProfileLevels(HostMediaRecorder.VideoEncoderName enco * @param supportedFps サポートしている fps のリスト */ private static void setSupportedFps(Intent response, List> supportedFps) { - List fpsList = new ArrayList<>(); - for (Range fps : supportedFps) { - if (!fpsList.contains(fps.getUpper())) { - fpsList.add(fps.getUpper()); - } - } - response.putExtra("frameRate", fpsList.toArray(new Integer[0])); + response.putExtra("frameRate", createSupportedFps(supportedFps)); + } + + /** + * カメラがサポートしている Fps のリストをレスポンスに格納します. + * + * @param bundle レスポンス + * @param supportedFps サポートしている fps のリスト + */ + private static void setSupportedFps(Bundle bundle, List> supportedFps) { + bundle.putStringArray("frameRate", createSupportedFps(supportedFps)); } /** diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractBroadcastProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractBroadcastProvider.java index df584347b9..b4e2053344 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractBroadcastProvider.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractBroadcastProvider.java @@ -13,200 +13,47 @@ import org.deviceconnect.android.deviceplugin.host.R; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.ArrayList; +import java.util.List; -public abstract class AbstractBroadcastProvider implements BroadcasterProvider { - /** - * 映像を配信するクラス. - */ - private Broadcaster mBroadcaster; - - /** - * イベントを通知するリスナー. - */ - private OnEventListener mOnEventListener; - - private final Context mContext; - private final HostMediaRecorder mRecorder; +public abstract class AbstractBroadcastProvider extends AbstractLiveStreamingProvider implements BroadcasterProvider { public AbstractBroadcastProvider(Context context, HostMediaRecorder 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; - } + super(context, recorder); } @Override - public void onConfigChange() { - if (mBroadcaster != null) { - mBroadcaster.onConfigChange(); + public List getSupportedMimeType() { + List mimeType = new ArrayList<>(); + for (LiveStreaming server : getLiveStreamingList()) { + mimeType.add(server.getMimeType()); } + return mimeType; } @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 + protected void hideNotification(String id) { + NotificationManager manager = (NotificationManager) getContext() .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) { + @Override + protected void sendNotification(String id, String name) { PendingIntent contentIntent = createPendingIntent(id); Notification notification = createNotification(contentIntent, null, name); - NotificationManager manager = (NotificationManager) mContext + NotificationManager manager = (NotificationManager) getContext() .getSystemService(Service.NOTIFICATION_SERVICE); if (manager != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - String channelId = mContext.getResources().getString(R.string.overlay_preview_channel_id); + String channelId = getContext().getResources().getString(R.string.overlay_preview_channel_id); NotificationChannel channel = new NotificationChannel( channelId, - mContext.getResources().getString(R.string.host_notification_recorder_broadcast), + getContext().getResources().getString(R.string.host_notification_recorder_broadcast), NotificationManager.IMPORTANCE_LOW); - channel.setDescription(mContext.getResources().getString(R.string.host_notification_recorder_broadcast_content)); + channel.setDescription(getContext().getResources().getString(R.string.host_notification_recorder_broadcast_content)); manager.createNotificationChannel(channel); notification = createNotification(contentIntent, channelId, name); } @@ -214,6 +61,15 @@ private void sendNotification(String id, String name) { } } + /** + * Notification の Id を取得します. + * + * @return Notification の Id + */ + private int getNotificationId() { + return 1000 + getRecorder().getId().hashCode(); + } + /** * Notificationを作成する. * @@ -224,25 +80,25 @@ private void sendNotification(String id, String name) { */ 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()); + NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext().getApplicationContext()); builder.setContentIntent(pendingIntent); - builder.setTicker(mContext.getString(R.string.host_notification_recorder_broadcast_ticker)); + builder.setTicker(getContext().getString(R.string.host_notification_recorder_broadcast_ticker)); builder.setSmallIcon(R.drawable.dconnect_icon); - builder.setContentTitle(mContext.getString(R.string.host_notification_recorder_broadcast, name)); - builder.setContentText(mContext.getString(R.string.host_notification_recorder_broadcast_content)); + builder.setContentTitle(getContext().getString(R.string.host_notification_recorder_broadcast, name)); + builder.setContentText(getContext().getString(R.string.host_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()); + Notification.Builder builder = new Notification.Builder(getContext().getApplicationContext()); builder.setContentIntent(pendingIntent); - builder.setTicker(mContext.getString(R.string.overlay_preview_ticker)); + builder.setTicker(getContext().getString(R.string.overlay_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.host_notification_recorder_broadcast, name)); - builder.setContentText(mContext.getString(R.string.host_notification_recorder_broadcast_content)); + builder.setContentTitle(getContext().getString(R.string.host_notification_recorder_broadcast, name)); + builder.setContentText(getContext().getString(R.string.host_notification_recorder_broadcast_content)); builder.setWhen(System.currentTimeMillis()); builder.setAutoCancel(true); builder.setOngoing(true); @@ -264,6 +120,6 @@ private PendingIntent createPendingIntent(String id) { Intent intent = new Intent(); intent.setAction(HostMediaRecorderManager.ACTION_STOP_BROADCAST); intent.putExtra(HostMediaRecorderManager.KEY_RECORDER_ID, id); - return PendingIntent.getBroadcast(mContext, getNotificationId(), intent, 0); + return PendingIntent.getBroadcast(getContext(), getNotificationId(), intent, 0); } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractBroadcaster.java index 9428381c08..2f4107ffec 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractBroadcaster.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractBroadcaster.java @@ -1,188 +1,44 @@ package org.deviceconnect.android.deviceplugin.host.recorder; -import android.graphics.Rect; -import android.media.AudioAttributes; -import android.media.AudioFormat; -import android.media.AudioPlaybackCaptureConfiguration; -import android.media.projection.MediaProjection; -import android.os.Build; -import android.util.Size; +import org.deviceconnect.android.libmedia.streaming.audio.AudioEncoder; +import org.deviceconnect.android.libmedia.streaming.audio.MicAACLATMEncoder; +import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; -import org.deviceconnect.android.deviceplugin.host.recorder.util.MediaProjectionProvider; -import org.deviceconnect.android.libmedia.streaming.audio.AudioQuality; -import org.deviceconnect.android.libmedia.streaming.audio.MicAudioQuality; -import org.deviceconnect.android.libmedia.streaming.audio.filter.HighPassFilter; -import org.deviceconnect.android.libmedia.streaming.audio.filter.LowPassFilter; -import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; -import org.deviceconnect.android.libmedia.streaming.video.VideoQuality; +public abstract class AbstractBroadcaster extends AbstractLiveStreaming implements Broadcaster { -public abstract class AbstractBroadcaster implements Broadcaster { - /** - * 配信先の URI. - */ - private final String mBroadcastURI; - - /** - * カメラを操作するレコーダ. - */ - private final HostMediaRecorder mRecorder; - - public AbstractBroadcaster(HostMediaRecorder recorder, String broadcastURI) { - mRecorder = recorder; - mBroadcastURI = broadcastURI; - } - - @Override - public String getMimeType() { - return ""; + public AbstractBroadcaster(HostMediaRecorder recorder, String id) { + super(recorder, id); } @Override public String getBroadcastURI() { - return mBroadcastURI; + return getEncoderSettings().getBroadcastURI(); } @Override - public void onConfigChange() { - VideoQuality videoQuality = getVideoQuality(); - if (videoQuality != null) { - setVideoQuality(videoQuality); - } - - AudioQuality audioQuality = getAudioQuality(); - if (audioQuality != null) { - setAudioQuality(audioQuality); - - HostMediaRecorder.Settings settings = getRecorder().getSettings(); - setMute(settings.isMute()); - } + public String getUri() { + return getEncoderSettings().getBroadcastURI(); } /** - * 映像の設定を取得します. + * 配信するための映像用エンコーダを取得します. * - * 映像が使用されていない場合は null を返却すること。 - * - * @return 映像の設定 + * @return 配信するための映像用エンコーダ */ - protected VideoQuality getVideoQuality() { + protected VideoEncoder createVideoEncoder() { return null; } /** - * 音声の設定を取得します. - * - * 音声が使用されていない場合は null を返却すること。 + * 配信するための音声用エンコーダを取得します. * - * @return 音声の設定 + * @return 配信するための音声用エンコーダ */ - protected AudioQuality getAudioQuality() { - return null; - } - - /** - * Broadcaster で使用するレコーダを取得します. - * - * @return Broadcaster で使用するレコーダ - */ - public HostMediaRecorder getRecorder() { - return mRecorder; - } - - /** - * VideoEncoder の設定に、HostMediaRecorder の設定を反映します. - * - * @param videoQuality 設定を行う VideoEncoder の VideoQuality - */ - public void setVideoQuality(VideoQuality videoQuality) { - HostMediaRecorder recorder = getRecorder(); - HostMediaRecorder.Settings settings = recorder.getSettings(); - - Rect rect = settings.getDrawingRange(); - if (rect != null) { - videoQuality.setVideoWidth(rect.width()); - videoQuality.setVideoHeight(rect.height()); - } else { - EGLSurfaceDrawingThread d = recorder.getSurfaceDrawingThread(); - Size previewSize = settings.getPreviewSize(); - int w = d.isSwappedDimensions() ? previewSize.getHeight() : previewSize.getWidth(); - int h = d.isSwappedDimensions() ? previewSize.getWidth() : previewSize.getHeight(); - videoQuality.setVideoWidth(w); - videoQuality.setVideoHeight(h); - } - 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()); - if (settings.getPreviewBitRateMode() != null) { - switch (settings.getPreviewBitRateMode()) { - case VBR: - videoQuality.setBitRateMode(VideoQuality.BitRateMode.VBR); - break; - case CBR: - videoQuality.setBitRateMode(VideoQuality.BitRateMode.CBR); - break; - case CQ: - videoQuality.setBitRateMode(VideoQuality.BitRateMode.CQ); - break; - } - } else { - videoQuality.setBitRateMode(null); - } - } - - /** - * AudioEncoder の設定に、HostMediaRecorder の設定を反映します. - * - * @param audioQuality 設定を行う AudioEncoder の AudioQuality - */ - public void setAudioQuality(AudioQuality audioQuality) { - HostMediaRecorder recorder = getRecorder(); - HostMediaRecorder.Settings settings = recorder.getSettings(); - - audioQuality.setChannel(settings.getPreviewChannel() == 1 ? - AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO); - audioQuality.setSamplingRate(settings.getPreviewSampleRate()); - audioQuality.setBitRate(settings.getPreviewAudioBitRate()); - audioQuality.setUseAEC(settings.isUseAEC()); - - if (settings.getAudioFilter() != null) { - float coeff = settings.getAudioCoefficient(); - switch (settings.getAudioFilter()) { - case LOW_PASS: - audioQuality.setFilter(new LowPassFilter(audioQuality, coeff)); - break; - case HIGH_PASS: - audioQuality.setFilter(new HighPassFilter(audioQuality, coeff)); - break; - default: - audioQuality.setFilter(null); - break; - } - } else { - audioQuality.setFilter(null); - } - - MicAudioQuality quality = (MicAudioQuality) audioQuality; - if (settings.getPreviewAudioSource() == HostMediaRecorder.AudioSource.APP) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // アプリの録音機能 - MediaProjectionProvider provider = recorder.getMediaProjectionProvider(); - if (provider != null && provider.getMediaProjection() != null) { - MediaProjection mediaProjection = provider.getMediaProjection(); - AudioPlaybackCaptureConfiguration configuration = - new AudioPlaybackCaptureConfiguration.Builder(mediaProjection) - .addMatchingUsage(AudioAttributes.USAGE_GAME) - .addMatchingUsage(AudioAttributes.USAGE_MEDIA) - .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN) - .build(); - quality.setCaptureConfig(configuration); - } - } - quality.setSource(MicAudioQuality.Source.APP); + protected AudioEncoder createAudioEncoder() { + HostMediaRecorder.Settings settings = getRecorder().getSettings(); + if (settings.isAudioEnabled()) { + return new MicAACLATMEncoder(); } + return null; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractLiveStreaming.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractLiveStreaming.java new file mode 100644 index 0000000000..9bb4562943 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractLiveStreaming.java @@ -0,0 +1,430 @@ +package org.deviceconnect.android.deviceplugin.host.recorder; + +import android.graphics.Rect; +import android.media.AudioAttributes; +import android.media.AudioFormat; +import android.media.AudioPlaybackCaptureConfiguration; +import android.media.projection.MediaProjection; +import android.os.Build; +import android.util.Size; + +import org.deviceconnect.android.deviceplugin.host.recorder.util.MediaProjectionProvider; +import org.deviceconnect.android.deviceplugin.host.recorder.util.MovingRectThread; +import org.deviceconnect.android.libmedia.streaming.audio.AudioEncoder; +import org.deviceconnect.android.libmedia.streaming.audio.AudioQuality; +import org.deviceconnect.android.libmedia.streaming.audio.MicAudioQuality; +import org.deviceconnect.android.libmedia.streaming.audio.filter.HighPassFilter; +import org.deviceconnect.android.libmedia.streaming.audio.filter.LowPassFilter; +import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; +import org.deviceconnect.android.libmedia.streaming.util.WeakReferenceList; +import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; +import org.deviceconnect.android.libmedia.streaming.video.VideoQuality; + +import javax.net.ssl.SSLContext; + +public abstract class AbstractLiveStreaming implements LiveStreaming, CropInterface { + /** + * カメラを操作するレコーダ. + */ + private final HostMediaRecorder mRecorder; + + /** + * SSLContext のインスタンス. + */ + private SSLContext mSSLContext; + + /** + * ミュート設定. + */ + private boolean mMute; + + /** + * プレビュー配信サーバID. + */ + private final String mId; + + /** + * 切り抜き範囲移動用スレッド. + */ + private MovingRectThread mMovingRectThread; + + /** + * 切り抜き範囲移動用スレッドからのイベントを受け取るリスナー. + */ + private final MovingRectThread.OnEventListener mMovingRectThreadOnEventListener = this::onUpdateCropRect; + + /** + * 切り抜き範囲のイベントを通知するリスナー. + */ + private final WeakReferenceList mOnEventListeners = new WeakReferenceList<>(); + + protected void onUpdateCropRect(Rect rect) { + VideoQuality videoQuality = getVideoQuality(); + if (videoQuality != null) { + videoQuality.setCropRect(new Rect(rect)); + } + getEncoderSettings().setCropRect(rect); + postOnMoved(rect); + } + + public AbstractLiveStreaming(HostMediaRecorder recorder, String id) { + mRecorder = recorder; + mId = id; + mMute = true; + startMovingRectThread(); + } + + @Override + public String getId() { + return mId; + } + + @Override + public void setOnEventListener(LiveStreaming.OnEventListener listener) { + } + + @Override + public void onConfigChange() { + VideoEncoder videoEncoder = getVideoEncoder(); + if (videoEncoder != null) { + setVideoQuality(videoEncoder.getVideoQuality()); + videoEncoder.restart(); + } + + AudioQuality audioQuality = getAudioQuality(); + if (audioQuality != null) { + setAudioQuality(audioQuality); + + HostMediaRecorder.Settings settings = getRecorder().getSettings(); + setMute(settings.isMute()); + } + } + + @Override + public void release() { + stop(); + stopMovingRectThread(); + } + + @Override + public String getUri() { + return null; + } + + @Override + public void setMute(boolean mute) { + mMute = mute; + } + + @Override + public boolean isMuted() { + return mMute; + } + + @Override + public boolean requestSyncFrame() { + VideoEncoder videoEncoder = getVideoEncoder(); + if (videoEncoder != null) { + return videoEncoder.requestSyncKeyFrame(); + } + return false; + } + + @Override + public boolean requestBitRate() { + VideoEncoder videoEncoder = getVideoEncoder(); + if (videoEncoder != null) { + videoEncoder.getVideoQuality().setBitRate(getEncoderSettings().getPreviewBitRate()); + return videoEncoder.requestBitRate(); + } + return false; + } + + @Override + public boolean requestJpegQuality() { + return false; + } + + @Override + public long getBPS() { + return 0; + } + + @Override + public boolean useSSLContext() { + return getEncoderSettings().isUseSSL(); + } + + @Override + public void setSSLContext(final SSLContext sslContext) { + mSSLContext = sslContext; + } + + @Override + public SSLContext getSSLContext() { + return mSSLContext; + } + + // CropInterface implements + + @Override + public String getName() { + return getEncoderSettings().getName(); + } + + @Override + public void moveCropRect(Rect start, Rect end, int duration) { + checkCropRect(end); + + if (end == null || mMovingRectThread == null) { + setCropRectInternal(end); + } else { + mMovingRectThread.move(start, end, duration); + } + } + + @Override + public void setCropRect(Rect rect) { + checkCropRect(rect); + + if (rect == null || mMovingRectThread == null) { + mMovingRectThread.cancelMove(); + setCropRectInternal(rect); + } else { + mMovingRectThread.set(rect); + } + } + + @Override + public Rect getCropRect() { + return getEncoderSettings().getCropRect(); + } + + @Override + public void addOnEventListener(CropInterface.OnEventListener listener) { + mOnEventListeners.add(listener); + } + + @Override + public void removeOnEventListener(CropInterface.OnEventListener listener) { + mOnEventListeners.remove(listener); + } + + private void checkCropRect(Rect rect) { + if (rect != null) { + if (getEncoderSettings().getCropRect() == null) { + postOnAdded(rect); + } + } else { + postOnRemove(); + } + } + + private void setCropRectInternal(Rect rect) { + getEncoderSettings().setCropRect(rect); + + VideoQuality videoQuality = getVideoQuality(); + if (videoQuality != null) { + videoQuality.setCropRect(rect); + } + } + + private void postOnAdded(Rect rect) { + for (CropInterface.OnEventListener l : mOnEventListeners.get()) { + l.onAdded(this, rect); + } + } + + private void postOnRemove() { + for (CropInterface.OnEventListener l : mOnEventListeners.get()) { + l.onRemoved(this); + } + } + + private void postOnMoved(Rect rect) { + for (CropInterface.OnEventListener l : mOnEventListeners.get()) { + l.onMoved(this, rect); + } + } + + private void startMovingRectThread() { + if (mMovingRectThread != null) { + return; + } + + mMovingRectThread = new MovingRectThread(); + mMovingRectThread.addOnEventListener(mMovingRectThreadOnEventListener); + mMovingRectThread.start(); + } + + private void stopMovingRectThread() { + if (mMovingRectThread != null) { + mMovingRectThread.stop(); + mMovingRectThread = null; + } + } + + /** + * プレビューを表示するレコーダー. + * + * @return レコーダー + */ + public HostMediaRecorder getRecorder() { + return mRecorder; + } + + /** + * Broadcaster の設定を取得します. + * + * @return サーバの設定 + */ + public HostMediaRecorder.EncoderSettings getEncoderSettings() { + return mRecorder.getSettings().getEncoderSetting(mId); + } + + /** + * 映像のエンコーダを取得します. + * + * 映像が使用されていない場合は null を返却すること。 + * + * @return 映像のエンコーダ + */ + protected VideoEncoder getVideoEncoder() { + return null; + } + + /** + * 音声のエンコーダを取得します. + * + * 音声が使用されていない場合は null を返却すること。 + * + * @return 音声のエンコーダ + */ + protected AudioEncoder getAudioEncoder() { + return null; + } + + /** + * 映像の設定を取得します. + * + * 映像が使用されていない場合は null を返却すること。 + * + * @return 映像の設定 + */ + protected VideoQuality getVideoQuality() { + VideoEncoder videoEncoder = getVideoEncoder(); + if (videoEncoder != null) { + return videoEncoder.getVideoQuality(); + } + return null; + } + + /** + * 音声の設定を取得します. + * + * 音声が使用されていない場合は null を返却すること。 + * + * @return 音声の設定 + */ + protected AudioQuality getAudioQuality() { + AudioEncoder audioEncoder = getAudioEncoder(); + if (audioEncoder != null) { + return audioEncoder.getAudioQuality(); + } + return null; + } + + /** + * VideoEncoder の設定に、HostMediaRecorder の設定を反映します. + * + * @param videoQuality 設定を行う VideoEncoder の VideoQuality + */ + public void setVideoQuality(VideoQuality videoQuality) { + HostMediaRecorder recorder = getRecorder(); + HostMediaRecorder.EncoderSettings settings = getEncoderSettings(); + + EGLSurfaceDrawingThread d = recorder.getSurfaceDrawingThread(); + Size previewSize = settings.getPreviewSize(); + int w = d.isSwappedDimensions() ? previewSize.getHeight() : previewSize.getWidth(); + int h = d.isSwappedDimensions() ? previewSize.getWidth() : previewSize.getHeight(); + videoQuality.setVideoWidth(w); + videoQuality.setVideoHeight(h); + videoQuality.setCropRect(settings.getCropRect()); + 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()); + if (settings.getPreviewBitRateMode() != null) { + switch (settings.getPreviewBitRateMode()) { + default: + case VBR: + videoQuality.setBitRateMode(VideoQuality.BitRateMode.VBR); + break; + case CBR: + videoQuality.setBitRateMode(VideoQuality.BitRateMode.CBR); + break; + case CQ: + videoQuality.setBitRateMode(VideoQuality.BitRateMode.CQ); + break; + } + } else { + videoQuality.setBitRateMode(null); + } + } + + /** + * AudioEncoder の設定に、HostMediaRecorder の設定を反映します. + * + * @param audioQuality 設定を行う AudioEncoder の AudioQuality + */ + public void setAudioQuality(AudioQuality audioQuality) { + HostMediaRecorder recorder = getRecorder(); + HostMediaRecorder.Settings settings = recorder.getSettings(); + + mMute = settings.isMute(); + audioQuality.setChannel(settings.getPreviewChannel() == 1 ? + AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO); + audioQuality.setSamplingRate(settings.getPreviewSampleRate()); + audioQuality.setBitRate(settings.getPreviewAudioBitRate()); + audioQuality.setUseAEC(settings.isUseAEC()); + + if (settings.getAudioFilter() != null) { + float coeff = settings.getAudioCoefficient(); + switch (settings.getAudioFilter()) { + case LOW_PASS: + audioQuality.setFilter(new LowPassFilter(audioQuality, coeff)); + break; + case HIGH_PASS: + audioQuality.setFilter(new HighPassFilter(audioQuality, coeff)); + break; + default: + audioQuality.setFilter(null); + break; + } + } else { + audioQuality.setFilter(null); + } + + MicAudioQuality quality = (MicAudioQuality) audioQuality; + if (settings.getPreviewAudioSource() == HostMediaRecorder.AudioSource.APP) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // アプリの録音機能 + MediaProjectionProvider provider = recorder.getMediaProjectionProvider(); + if (provider != null && provider.getMediaProjection() != null) { + MediaProjection mediaProjection = provider.getMediaProjection(); + AudioPlaybackCaptureConfiguration configuration = + new AudioPlaybackCaptureConfiguration.Builder(mediaProjection) + .addMatchingUsage(AudioAttributes.USAGE_GAME) + .addMatchingUsage(AudioAttributes.USAGE_MEDIA) + .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN) + .build(); + quality.setCaptureConfig(configuration); + } + } + quality.setSource(MicAudioQuality.Source.APP); + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractLiveStreamingProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractLiveStreamingProvider.java new file mode 100644 index 0000000000..9a5877b0ec --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractLiveStreamingProvider.java @@ -0,0 +1,246 @@ +package org.deviceconnect.android.deviceplugin.host.recorder; + +import android.content.Context; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public abstract class AbstractLiveStreamingProvider implements LiveStreamingProvider { + /** + * コンテキスト. + */ + private final Context mContext; + + /** + * プレビュー配信サーバーのリスト. + */ + private final List mLiveStreamingList = new ArrayList<>(); + + /** + * プレビュー配信を行うレコーダ. + */ + private final HostMediaRecorder mRecorder; + + /** + * Notification 表示フラグ. + */ + private boolean mIsRunning; + + /** + * プレビュー配信サーバのイベントを通知するリスナー. + */ + private OnEventListener mOnEventListener; + + public AbstractLiveStreamingProvider(Context context, HostMediaRecorder recorder) { + mContext = context; + mRecorder = recorder; + init(); + } + + @Override + public void addLiveStreaming(LiveStreaming streaming) { + if (streaming != null) { + mLiveStreamingList.add(streaming); + } + } + + @Override + public void removeLiveStreaming(String encoderId) { + for (LiveStreaming streaming : mLiveStreamingList) { + if (streaming.getId().equals(encoderId)) { + streaming.release(); + mLiveStreamingList.remove(streaming); + return; + } + } + } + + @Override + public List getLiveStreamingList() { + return mLiveStreamingList; + } + + @Override + public boolean isRunning() { + return mIsRunning; + } + + @Override + public List start() { + List results = new ArrayList<>(); + + CountDownLatch latch = new CountDownLatch(mLiveStreamingList.size()); + for (LiveStreaming streaming : mLiveStreamingList) { + streaming.setOnEventListener(new Broadcaster.OnEventListener() { + @Override + public void onStarted() { + } + + @Override + public void onStopped() { + } + + @Override + public void onError(Exception e) { + postOnError(streaming, e); + + if (isAllStreamingStopped()) { + stop(); + } + } + }); + streaming.start(new LiveStreaming.OnStartCallback() { + @Override + public void onSuccess() { + results.add(streaming); + latch.countDown(); + } + + @Override + public void onFailed(Exception e) { + latch.countDown(); + } + }); + } + + try { + latch.await(5, TimeUnit.SECONDS); + if (results.size() > 0) { + mIsRunning = true; + sendNotification(mRecorder.getId(), mRecorder.getName()); + postOnStarted(results); + } + } catch (InterruptedException e) { + // ignore. + } + return results; + } + + @Override + public void stop() { + hideNotification(mRecorder.getId()); + + for (LiveStreaming streaming : getLiveStreamingList()) { + streaming.stop(); + } + + if (mIsRunning) { + mIsRunning = false; + postOnStopped(); + } + } + + @Override + public void requestSyncFrame() { + for (LiveStreaming streaming : getLiveStreamingList()) { + streaming.requestSyncFrame(); + } + } + + @Override + public void requestBitRate() { + for (LiveStreaming streaming : getLiveStreamingList()) { + streaming.requestBitRate(); + } + } + + @Override + public void requestJpegQuality() { + for (LiveStreaming streaming : getLiveStreamingList()) { + streaming.requestJpegQuality(); + } + } + + @Override + public void onConfigChange() { + for (LiveStreaming streaming : getLiveStreamingList()) { + streaming.onConfigChange(); + } + } + + @Override + public void setMute(boolean mute) { + for (LiveStreaming streaming : getLiveStreamingList()) { + streaming.setMute(mute); + } + } + + @Override + public void setOnEventListener(OnEventListener listener) { + mOnEventListener = listener; + } + + @Override + public void release() { + stop(); + + for (LiveStreaming streaming : getLiveStreamingList()) { + streaming.release(); + } + } + + /** + * Notification を送信します. + * + * @param id notification を識別する ID + * @param name 名前 + */ + protected abstract void sendNotification(String id, String name); + + /** + * Notification 非表示にします. + * + * @param id notification を識別する ID + */ + protected abstract void hideNotification(String id); + + public Context getContext() { + return mContext; + } + + public HostMediaRecorder getRecorder() { + return mRecorder; + } + + private void init() { + HostMediaRecorder.Settings settings = getRecorder().getSettings(); + for (String encoderId : settings.getEncoderIdList()) { + HostMediaRecorder.EncoderSettings encoderSetting = settings.getEncoderSetting(encoderId); + if (encoderSetting != null) { + LiveStreaming streaming = createLiveStreaming(encoderId, encoderSetting); + if (streaming != null) { + addLiveStreaming(streaming); + } + } + } + } + + private boolean isAllStreamingStopped() { + for (LiveStreaming streaming : mLiveStreamingList) { + if (streaming.isRunning()) { + return true; + } + } + return true; + } + + private void postOnStarted(List servers) { + if (mOnEventListener != null) { + mOnEventListener.onStarted(servers); + } + } + + private void postOnStopped() { + if (mOnEventListener != null) { + mOnEventListener.onStopped(); + } + } + + private void postOnError(LiveStreaming server, Exception e) { + if (mOnEventListener != null) { + mOnEventListener.onError(server, e); + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractMJPEGPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractMJPEGPreviewServer.java index 22804c2584..b86d76adfe 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractMJPEGPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractMJPEGPreviewServer.java @@ -1,6 +1,5 @@ package org.deviceconnect.android.deviceplugin.host.recorder; -import android.content.Context; import android.graphics.Rect; import android.util.Log; import android.util.Size; @@ -10,7 +9,6 @@ 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; @@ -19,7 +17,7 @@ public abstract class AbstractMJPEGPreviewServer extends AbstractPreviewServer { /** * Motion JPEG のマイムタイプを定義します. */ - protected static final String MIME_TYPE = "video/x-mjpeg"; + public static final String MIME_TYPE = "video/x-mjpeg"; /** * サーバー名を定義します. @@ -31,8 +29,19 @@ public abstract class AbstractMJPEGPreviewServer extends AbstractPreviewServer { */ private MJPEGServer mMJPEGServer; - public AbstractMJPEGPreviewServer(Context context, HostMediaRecorder recorder, boolean useSSL) { - super(context, recorder, useSSL); + public AbstractMJPEGPreviewServer(HostMediaRecorder recorder, String encoderId) { + super(recorder, encoderId); + } + + @Override + protected void onUpdateCropRect(Rect rect) { + if (mMJPEGServer != null) { + MJPEGEncoder encoder = mMJPEGServer.getMJPEGEncoder(); + if (encoder != null) { + encoder.getMJPEGQuality().setCropRect(rect); + } + } + super.onUpdateCropRect(rect); } // PreviewServer @@ -48,17 +57,22 @@ public String getMimeType() { } @Override - public void startWebServer(final OnWebServerStartCallback callback) { + public boolean isRunning() { + return mMJPEGServer != null; + } + + @Override + public void start(final OnStartCallback callback) { if (mMJPEGServer == null) { SSLContext sslContext = getSSLContext(); if (useSSLContext() && sslContext == null) { - callback.onFail(); + callback.onFailed(new RuntimeException("Failed to create a SSLContext.")); return; } mMJPEGServer = new MJPEGServer(); mMJPEGServer.setServerName(SERVER_NAME); - mMJPEGServer.setServerPort(getPort()); + mMJPEGServer.setServerPort(getEncoderSettings().getPort()); mMJPEGServer.setCallback(mCallback); if (useSSLContext()) { mMJPEGServer.setSSLContext(sslContext); @@ -66,15 +80,15 @@ public void startWebServer(final OnWebServerStartCallback callback) { try { mMJPEGServer.start(); } catch (Exception e) { - callback.onFail(); + callback.onFailed(e); return; } } - callback.onStart(getUri()); + callback.onSuccess(); } @Override - public void stopWebServer() { + public void stop() { if (mMJPEGServer != null) { mMJPEGServer.stop(); mMJPEGServer = null; @@ -82,14 +96,20 @@ public void stopWebServer() { } @Override - public boolean requestSyncFrame() { - // 何もしない - return false; + public long getBPS() { + return mMJPEGServer != null ? mMJPEGServer.getBPS() : 0; } @Override - public long getBPS() { - return mMJPEGServer != null ? mMJPEGServer.getBPS() : 0; + public boolean requestJpegQuality() { + if (mMJPEGServer != null) { + MJPEGEncoder encoder = mMJPEGServer.getMJPEGEncoder(); + if (encoder != null) { + encoder.getMJPEGQuality().setQuality(getEncoderSettings().getPreviewQuality()); + return true; + } + } + return false; } @Override @@ -130,22 +150,20 @@ private void restartEncoder() { */ private void setMJPEGQuality(MJPEGQuality quality) { HostMediaRecorder recorder = getRecorder(); - HostMediaRecorder.Settings settings = recorder.getSettings(); - - Rect rect = settings.getDrawingRange(); - if (rect != null) { - quality.setWidth(rect.width()); - quality.setHeight(rect.height()); - } else { - EGLSurfaceDrawingThread d = recorder.getSurfaceDrawingThread(); - Size previewSize = settings.getPreviewSize(); - int w = d.isSwappedDimensions() ? previewSize.getHeight() : previewSize.getWidth(); - int h = d.isSwappedDimensions() ? previewSize.getWidth() : previewSize.getHeight(); - quality.setWidth(w); - quality.setHeight(h); + HostMediaRecorder.EncoderSettings settings = getEncoderSettings(); + + EGLSurfaceDrawingThread d = recorder.getSurfaceDrawingThread(); + Size previewSize = settings.getPreviewSize(); + if (previewSize == null) { + previewSize = settings.getPreviewSize(); } - quality.setFrameRate(settings.getPreviewMaxFrameRate()); + int w = d.isSwappedDimensions() ? previewSize.getHeight() : previewSize.getWidth(); + int h = d.isSwappedDimensions() ? previewSize.getWidth() : previewSize.getHeight(); + quality.setWidth(w); + quality.setHeight(h); quality.setQuality(settings.getPreviewQuality()); + quality.setFrameRate(settings.getPreviewMaxFrameRate()); + quality.setCropRect(settings.getCropRect()); } /** diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractMediaRecorder.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractMediaRecorder.java index dcc562618b..59b031699b 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractMediaRecorder.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractMediaRecorder.java @@ -25,6 +25,7 @@ import org.deviceconnect.android.deviceplugin.host.recorder.util.MediaProjectionProvider; import org.deviceconnect.android.deviceplugin.host.recorder.util.MediaSharing; import org.deviceconnect.android.deviceplugin.host.recorder.util.SurfaceMP4Recorder; +import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; import org.deviceconnect.android.provider.FileManager; import java.io.File; @@ -103,19 +104,19 @@ public AbstractMediaRecorder(Context context, FileManager fileManager, MediaProj public void initialize() { BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); if (broadcasterProvider != null) { - broadcasterProvider.setOnEventListener(new BroadcasterProvider.OnEventListener() { + broadcasterProvider.setOnEventListener(new LiveStreamingProvider.OnEventListener() { @Override - public void onStarted(Broadcaster broadcaster) { - postOnBroadcasterStarted(broadcaster); + public void onStarted(List broadcasters) { + postOnBroadcasterStarted(broadcasters); } @Override - public void onStopped(Broadcaster broadcaster) { - postOnBroadcasterStopped(broadcaster); + public void onStopped() { + postOnBroadcasterStopped(); } @Override - public void onError(Broadcaster broadcaster, Exception e) { + public void onError(LiveStreaming broadcaster, Exception e) { postOnBroadcasterError(broadcaster, e); } }); @@ -123,9 +124,9 @@ public void onError(Broadcaster broadcaster, Exception e) { PreviewServerProvider previewProvider = getServerProvider(); if (previewProvider != null) { - previewProvider.setOnEventListener(new PreviewServerProvider.OnEventListener() { + previewProvider.setOnEventListener(new LiveStreamingProvider.OnEventListener() { @Override - public void onStarted(List servers) { + public void onStarted(List servers) { postOnPreviewStarted(servers); } @@ -135,7 +136,7 @@ public void onStopped() { } @Override - public void onError(PreviewServer server, Exception e) { + public void onError(LiveStreaming server, Exception e) { postOnPreviewError(e); } }); @@ -149,6 +150,18 @@ public void clean() { @Override public void destroy() { + BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); + if (broadcasterProvider != null) { + broadcasterProvider.release(); + } + + PreviewServerProvider previewProvider = getServerProvider(); + if (previewProvider != null) { + previewProvider.release(); + } + + stopRecordingInternal(null); + mRequestHandler.getLooper().quit(); } @@ -165,25 +178,27 @@ public boolean isPreviewRunning() { @Override public void onDisplayRotation(int rotation) { - PreviewServerProvider previewServerProvider = getServerProvider(); - if (previewServerProvider != null) { - previewServerProvider.onConfigChange(); - } - - BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); - if (broadcasterProvider != null) { - broadcasterProvider.onConfigChange(); + if (getSettings().getOrientation() == -1) { + onConfigChange(); } } @Override public void onConfigChange() { PreviewServerProvider previewServerProvider = getServerProvider(); + BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); + + // 設定が変更された場合には、一度描画用のスレッドを停止しておく + EGLSurfaceDrawingThread drawingThread = getSurfaceDrawingThread(); + if (drawingThread != null && drawingThread.isRunning() + && (previewServerProvider.isRunning() || broadcasterProvider.isRunning())) { + drawingThread.stop(true); + } + if (previewServerProvider != null) { previewServerProvider.onConfigChange(); } - BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); if (broadcasterProvider != null) { broadcasterProvider.onConfigChange(); } @@ -192,13 +207,13 @@ public void onConfigChange() { } @Override - public List startPreview() { + public List startPreview() { PreviewServerProvider provider = getServerProvider(); if (provider == null) { return new ArrayList<>(); } - List servers = provider.startServers(); + List servers = provider.start(); if (!servers.isEmpty()) { provider.setMute(getSettings().isMute()); } @@ -209,7 +224,7 @@ public List startPreview() { public void stopPreview() { PreviewServerProvider provider = getServerProvider(); if (provider != null) { - provider.stopServers(); + provider.stop(); } } @@ -220,28 +235,24 @@ public boolean isBroadcasterRunning() { } @Override - public Broadcaster startBroadcaster(String uri) { - if (uri == null) { - return null; - } - + public List startBroadcaster(String uri) { BroadcasterProvider provider = getBroadcasterProvider(); if (provider == null) { - return null; + return new ArrayList<>(); } - Broadcaster broadcaster = provider.startBroadcaster(uri); - if (broadcaster != null) { + List broadcasters = provider.start(); + for (LiveStreaming broadcaster : broadcasters) { broadcaster.setMute(getSettings().isMute()); } - return broadcaster; + return broadcasters; } @Override public void stopBroadcaster() { BroadcasterProvider provider = getBroadcasterProvider(); if (provider != null) { - provider.stopBroadcaster(); + provider.stop(); } } @@ -251,6 +262,32 @@ public void requestKeyFrame() { if (previewProvider != null) { previewProvider.requestSyncFrame(); } + + BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); + if (broadcasterProvider != null) { + broadcasterProvider.requestSyncFrame(); + } + } + + @Override + public void requestBitRate() { + PreviewServerProvider previewProvider = getServerProvider(); + if (previewProvider != null) { + previewProvider.requestBitRate(); + } + + BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); + if (broadcasterProvider != null) { + broadcasterProvider.requestBitRate(); + } + } + + @Override + public void requestJpegQuality() { + PreviewServerProvider previewProvider = getServerProvider(); + if (previewProvider != null) { + previewProvider.requestJpegQuality(); + } } @Override @@ -280,8 +317,10 @@ public boolean isMute() { public void setSSLContext(SSLContext sslContext) { PreviewServerProvider previewProvider = getServerProvider(); if (previewProvider != null) { - for (PreviewServer server : previewProvider.getServers()) { - server.setSSLContext(sslContext); + for (LiveStreaming server : previewProvider.getLiveStreamingList()) { + if (server instanceof PreviewServer) { + ((PreviewServer) server).setSSLContext(sslContext); + } } } } @@ -293,16 +332,24 @@ public void setOnEventListener(OnEventListener listener) { @Override public long getBPS() { + long bps = 0; + PreviewServerProvider previewProvider = getServerProvider(); - if (previewProvider == null) { - return 0; + if (previewProvider != null) { + List servers = previewProvider.getLiveStreamingList(); + for (LiveStreaming streaming : servers) { + bps += streaming.getBPS(); + } } - long bps = 0; - List servers = previewProvider.getServers(); - for (PreviewServer previewServer : servers) { - bps += previewServer.getBPS(); + BroadcasterProvider broadcasterProvider = getBroadcasterProvider(); + if (broadcasterProvider != null) { + List broadcasters = broadcasterProvider.getLiveStreamingList(); + for (LiveStreaming streaming : broadcasters) { + bps += streaming.getBPS(); + } } + return bps; } @@ -381,22 +428,61 @@ protected void postRequestHandler(Runnable run) { } /** - * Runnable を順番に実行します. + * レコーダの状態を設定します. * - * @param run 実行する Runnable - * @param delay 実行するまでの遅延 + * @param state レコーダの状態 */ - protected void postRequestHandler(Runnable run, long delay) { - mRequestHandler.postDelayed(run, delay); + protected void setState(State state) { + mState = state; } /** - * レコーダの状態を設定します. + * エンコーダを追加します. * - * @param state レコーダの状態 + * @param encoderId エンコーダID + * @param encoderSettings エンコーダの設定 */ - protected void setState(State state) { - mState = state; + public void addEncoder(String encoderId, EncoderSettings encoderSettings) { + getSettings().addEncoder(encoderId); + + switch (encoderSettings.getMimeType()) { + case MJPEG: + case RTSP: + case SRT: { + LiveStreaming streaming = getServerProvider().createLiveStreaming(encoderId, encoderSettings); + if (streaming != null) { + getServerProvider().addLiveStreaming(streaming); + } + } break; + case RTMP: { + LiveStreaming streaming = getBroadcasterProvider().createLiveStreaming(encoderId, encoderSettings); + if (streaming != null) { + getBroadcasterProvider().addLiveStreaming(streaming); + } + } break; + } + } + + /** + * エンコーダを削除します. + * + * @param encoderId エンコーダ + */ + public void removeEncoder(String encoderId) { + if (getSettings().existEncoderId(encoderId)) { + EncoderSettings encoderSettings = getSettings().getEncoderSetting(encoderId); + switch (encoderSettings.getMimeType()) { + case MJPEG: + case RTSP: + case SRT: + getServerProvider().removeLiveStreaming(encoderId); + break; + case RTMP: + getBroadcasterProvider().removeLiveStreaming(encoderId); + break; + } + getSettings().removeEncoder(encoderId); + } } /** @@ -417,6 +503,12 @@ public FileManager getFileManager() { return mFileManager; } + /** + * 指定されたパーミッションの要求を行います. + * + * @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() { @@ -432,6 +524,11 @@ public void onFail(@NonNull String deniedPermission) { }); } + /** + * MediaProjection の許可要求を行います. + * + * @param callback 要求の結果を通知するコールバック + */ protected void requestMediaProjection(PermissionCallback callback) { getMediaProjectionProvider().requestPermission(new MediaProjectionProvider.Callback() { @Override @@ -647,7 +744,7 @@ protected void postOnConfigChanged() { } } - protected void postOnPreviewStarted(List servers) { + protected void postOnPreviewStarted(List servers) { if (mOnEventListener != null) { mOnEventListener.onPreviewStarted(servers); } @@ -665,19 +762,19 @@ protected void postOnPreviewError(Exception e) { } } - protected void postOnBroadcasterStarted(Broadcaster broadcaster) { + protected void postOnBroadcasterStarted(List broadcasters) { if (mOnEventListener != null) { - mOnEventListener.onBroadcasterStarted(broadcaster); + mOnEventListener.onBroadcasterStarted(broadcasters); } } - protected void postOnBroadcasterStopped(Broadcaster broadcaster) { + protected void postOnBroadcasterStopped() { if (mOnEventListener != null) { - mOnEventListener.onBroadcasterStopped(broadcaster); + mOnEventListener.onBroadcasterStopped(); } } - protected void postOnBroadcasterError(Broadcaster broadcaster, Exception e) { + protected void postOnBroadcasterError(LiveStreaming broadcaster, Exception e) { if (mOnEventListener != null) { mOnEventListener.onBroadcasterError(broadcaster, e); } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractPreviewServer.java index e89537d3d1..d76ae3ab25 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractPreviewServer.java @@ -1,284 +1,27 @@ package org.deviceconnect.android.deviceplugin.host.recorder; import android.content.Context; -import android.graphics.Rect; -import android.media.AudioAttributes; -import android.media.AudioFormat; -import android.media.AudioPlaybackCaptureConfiguration; -import android.media.projection.MediaProjection; -import android.os.Build; -import android.util.Size; import org.deviceconnect.android.deviceplugin.host.BuildConfig; -import org.deviceconnect.android.deviceplugin.host.recorder.util.MediaProjectionProvider; -import org.deviceconnect.android.libmedia.streaming.audio.AudioQuality; -import org.deviceconnect.android.libmedia.streaming.audio.MicAudioQuality; -import org.deviceconnect.android.libmedia.streaming.audio.filter.HighPassFilter; -import org.deviceconnect.android.libmedia.streaming.audio.filter.LowPassFilter; -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 { +public abstract class AbstractPreviewServer extends AbstractLiveStreaming implements PreviewServer { protected static final boolean DEBUG = BuildConfig.DEBUG; protected static final String TAG = "host.dplugin"; - /** - * コンテキスト. - */ - private final Context mContext; - - /** - * プレビュー再生を行うレコーダ. - */ - private final HostMediaRecorder mHostMediaRecorder; - - /** - * プレビュー配信サーバのポート番号. - */ - 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, HostMediaRecorder recorder) { - this(context, recorder, false); - } - /** * コンストラクタ. * *

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

* - * @param context コンテキスト * @param recorder プレビューで表示するレコーダ - * @param useSSL SSL使用フラグ - */ - public AbstractPreviewServer(Context context, HostMediaRecorder recorder, boolean useSSL) { - mContext = context; - mHostMediaRecorder = 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); - - HostMediaRecorder.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 HostMediaRecorder getRecorder() { - return mHostMediaRecorder; - } - - /** - * 映像の設定を取得します. - * - * 映像が使用されていない場合は null を返却すること。 - * - * @return 映像の設定 + * @param id 名前 */ - protected VideoQuality getVideoQuality() { - return null; - } - - /** - * 音声の設定を取得します. - * - * 音声が使用されていない場合は null を返却すること。 - * - * @return 音声の設定 - */ - protected AudioQuality getAudioQuality() { - return null; - } - - /** - * VideoEncoder の設定に、HostMediaRecorder の設定を反映します. - * - * @param videoQuality 設定を行う VideoEncoder の VideoQuality - */ - public void setVideoQuality(VideoQuality videoQuality) { - HostMediaRecorder recorder = getRecorder(); - HostMediaRecorder.Settings settings = recorder.getSettings(); - - Rect rect = settings.getDrawingRange(); - if (rect != null) { - videoQuality.setVideoWidth(rect.width()); - videoQuality.setVideoHeight(rect.height()); - } else { - EGLSurfaceDrawingThread d = recorder.getSurfaceDrawingThread(); - Size previewSize = settings.getPreviewSize(); - int w = d.isSwappedDimensions() ? previewSize.getHeight() : previewSize.getWidth(); - int h = d.isSwappedDimensions() ? previewSize.getWidth() : previewSize.getHeight(); - videoQuality.setVideoWidth(w); - videoQuality.setVideoHeight(h); - } - 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()); - if (settings.getPreviewBitRateMode() != null) { - switch (settings.getPreviewBitRateMode()) { - default: - case VBR: - videoQuality.setBitRateMode(VideoQuality.BitRateMode.VBR); - break; - case CBR: - videoQuality.setBitRateMode(VideoQuality.BitRateMode.CBR); - break; - case CQ: - videoQuality.setBitRateMode(VideoQuality.BitRateMode.CQ); - break; - } - } else { - videoQuality.setBitRateMode(null); - } - } - - /** - * AudioEncoder の設定に、HostMediaRecorder の設定を反映します. - * - * @param audioQuality 設定を行う AudioEncoder の AudioQuality - */ - public void setAudioQuality(AudioQuality audioQuality) { - HostMediaRecorder recorder = getRecorder(); - HostMediaRecorder.Settings settings = recorder.getSettings(); - - mMute = settings.isMute(); - audioQuality.setChannel(settings.getPreviewChannel() == 1 ? - AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO); - audioQuality.setSamplingRate(settings.getPreviewSampleRate()); - audioQuality.setBitRate(settings.getPreviewAudioBitRate()); - audioQuality.setUseAEC(settings.isUseAEC()); - - if (settings.getAudioFilter() != null) { - float coeff = settings.getAudioCoefficient(); - switch (settings.getAudioFilter()) { - case LOW_PASS: - audioQuality.setFilter(new LowPassFilter(audioQuality, coeff)); - break; - case HIGH_PASS: - audioQuality.setFilter(new HighPassFilter(audioQuality, coeff)); - break; - default: - audioQuality.setFilter(null); - break; - } - } else { - audioQuality.setFilter(null); - } - - MicAudioQuality quality = (MicAudioQuality) audioQuality; - if (settings.getPreviewAudioSource() == HostMediaRecorder.AudioSource.APP) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // アプリの録音機能 - MediaProjectionProvider provider = recorder.getMediaProjectionProvider(); - if (provider != null && provider.getMediaProjection() != null) { - MediaProjection mediaProjection = provider.getMediaProjection(); - AudioPlaybackCaptureConfiguration configuration = - new AudioPlaybackCaptureConfiguration.Builder(mediaProjection) - .addMatchingUsage(AudioAttributes.USAGE_GAME) - .addMatchingUsage(AudioAttributes.USAGE_MEDIA) - .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN) - .build(); - quality.setCaptureConfig(configuration); - } - } - quality.setSource(MicAudioQuality.Source.APP); - } + public AbstractPreviewServer(HostMediaRecorder recorder, String id) { + super(recorder, id); } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractPreviewServerProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractPreviewServerProvider.java index 8a5681367e..eee047fb67 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractPreviewServerProvider.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractPreviewServerProvider.java @@ -15,55 +15,26 @@ import android.content.Intent; import android.os.Build; -import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import org.deviceconnect.android.deviceplugin.host.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 HostMediaRecorder mRecorder; - - /** - * Notification 表示フラグ. - */ - private boolean mIsRunning; - - /** - * プレビュー配信サーバのイベントを通知するリスナー. - */ - private OnEventListener mOnEventListener; +public abstract class AbstractPreviewServerProvider extends AbstractLiveStreamingProvider implements PreviewServerProvider { /** * コンストラクタ. * @param context コンテキスト */ public AbstractPreviewServerProvider(final Context context, final HostMediaRecorder recorder) { - mContext = context; - mRecorder = recorder; - mIsRunning = false; + super(context, recorder); } // PreviewServerProvider @@ -71,173 +42,35 @@ public AbstractPreviewServerProvider(final Context context, final HostMediaRecor @Override public List getSupportedMimeType() { List mimeType = new ArrayList<>(); - for (PreviewServer server : getServers()) { + for (LiveStreaming server : getLiveStreamingList()) { 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 + protected void hideNotification(String id) { + NotificationManager manager = (NotificationManager) getContext() .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) { + @Override + protected void sendNotification(String id, String name) { PendingIntent contentIntent = createPendingIntent(id); Notification notification = createNotification(contentIntent, null, name); - NotificationManager manager = (NotificationManager) mContext + NotificationManager manager = (NotificationManager) getContext() .getSystemService(Service.NOTIFICATION_SERVICE); if (manager != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - String channelId = mContext.getResources().getString(R.string.overlay_preview_channel_id); + String channelId = getContext().getResources().getString(R.string.overlay_preview_channel_id); NotificationChannel channel = new NotificationChannel( channelId, - mContext.getResources().getString(R.string.host_notification_recorder_preview), + getContext().getResources().getString(R.string.host_notification_recorder_preview), NotificationManager.IMPORTANCE_LOW); - channel.setDescription(mContext.getResources().getString(R.string.host_notification_recorder_preview_content)); + channel.setDescription(getContext().getResources().getString(R.string.host_notification_recorder_preview_content)); manager.createNotificationChannel(channel); notification = createNotification(contentIntent, channelId, name); } @@ -245,6 +78,15 @@ private void sendNotification(String id, String name) { } } + /** + * Notification の Id を取得します. + * + * @return Notification の Id + */ + protected int getNotificationId() { + return 100 + getRecorder().getId().hashCode(); + } + /** * Notificationを作成する. * @@ -255,25 +97,25 @@ private void sendNotification(String id, String name) { */ 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()); + NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext().getApplicationContext()); builder.setContentIntent(pendingIntent); - builder.setTicker(mContext.getString(R.string.host_notification_recorder_preview_ticker)); + builder.setTicker(getContext().getString(R.string.host_notification_recorder_preview_ticker)); builder.setSmallIcon(R.drawable.dconnect_icon); - builder.setContentTitle(mContext.getString(R.string.host_notification_recorder_preview, name)); - builder.setContentText(mContext.getString(R.string.host_notification_recorder_preview_content)); + builder.setContentTitle(getContext().getString(R.string.host_notification_recorder_preview, name)); + builder.setContentText(getContext().getString(R.string.host_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()); + Notification.Builder builder = new Notification.Builder(getContext().getApplicationContext()); builder.setContentIntent(pendingIntent); - builder.setTicker(mContext.getString(R.string.overlay_preview_ticker)); + builder.setTicker(getContext().getString(R.string.overlay_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.host_notification_recorder_preview, name)); - builder.setContentText(mContext.getString(R.string.host_notification_recorder_preview_content)); + builder.setContentTitle(getContext().getString(R.string.host_notification_recorder_preview, name)); + builder.setContentText(getContext().getString(R.string.host_notification_recorder_preview_content)); builder.setWhen(System.currentTimeMillis()); builder.setAutoCancel(true); builder.setOngoing(true); @@ -295,6 +137,6 @@ private PendingIntent createPendingIntent(String id) { Intent intent = new Intent(); intent.setAction(HostMediaRecorderManager.ACTION_STOP_PREVIEW); intent.putExtra(HostMediaRecorderManager.KEY_RECORDER_ID, id); - return PendingIntent.getBroadcast(mContext, getNotificationId(), intent, 0); + return PendingIntent.getBroadcast(getContext(), getNotificationId(), intent, 0); } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractRTMPBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractRTMPBroadcaster.java index b6d23927db..01f813aeeb 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractRTMPBroadcaster.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractRTMPBroadcaster.java @@ -2,14 +2,10 @@ 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 配信クライアント. */ @@ -18,36 +14,19 @@ public abstract class AbstractRTMPBroadcaster extends AbstractBroadcaster { /** * イベントを通知するためのリスナー. */ - private OnEventListener mOnBroadcasterEventListener; - - public AbstractRTMPBroadcaster(HostMediaRecorder recorder, String broadcastURI) { - super(recorder, broadcastURI); - } + private Broadcaster.OnEventListener mOnBroadcasterEventListener; - /** - * RTMP で配信するための映像用エンコーダを取得します. - * - * @return RTMP で配信するための映像用エンコーダ - */ - protected VideoEncoder createVideoEncoder() { - return null; + public AbstractRTMPBroadcaster(HostMediaRecorder recorder, String encoderId) { + super(recorder, encoderId); } - /** - * RTMP で配信するための音声用エンコーダを取得します. - * - * @return RTMP で配信するための音声用エンコーダ - */ - protected AudioEncoder createAudioEncoder() { - HostMediaRecorder.Settings settings = getRecorder().getSettings(); - if (settings.isAudioEnabled()) { - return new MicAACLATMEncoder(); - } - return null; + @Override + public String getMimeType() { + return "video/x-rtmp"; } @Override - public void setOnEventListener(OnEventListener listener) { + public void setOnEventListener(Broadcaster.OnEventListener listener) { mOnBroadcasterEventListener = listener; } @@ -58,6 +37,12 @@ public boolean isRunning() { @Override public void start(OnStartCallback callback) { + String broadcastURI = getBroadcastURI(); + if (broadcastURI == null) { + callback.onFailed(new RuntimeException("broadcastURI is not set.")); + return; + } + VideoEncoder videoEncoder = createVideoEncoder(); if (videoEncoder != null) { setVideoQuality(videoEncoder.getVideoQuality()); @@ -68,9 +53,9 @@ public void start(OnStartCallback callback) { setAudioQuality(audioEncoder.getAudioQuality()); } - HostMediaRecorder.Settings settings = getRecorder().getSettings(); + HostMediaRecorder.EncoderSettings settings = getEncoderSettings(); - mRtmpClient = new RtmpClient(getBroadcastURI()); + mRtmpClient = new RtmpClient(broadcastURI); mRtmpClient.setMaxRetryCount(settings.getRetryCount()); mRtmpClient.setRetryInterval(settings.getRetryInterval()); mRtmpClient.setVideoEncoder(videoEncoder); @@ -132,44 +117,25 @@ public void stop() { @Override public void setMute(boolean mute) { + super.setMute(mute); + if (mRtmpClient != null) { mRtmpClient.setMute(mute); } } @Override - public boolean isMute() { + public boolean isMuted() { 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; + protected VideoEncoder getVideoEncoder() { + return mRtmpClient != null ? mRtmpClient.getVideoEncoder() : null; } @Override - protected AudioQuality getAudioQuality() { - if (mRtmpClient != null) { - AudioEncoder audioEncoder = mRtmpClient.getAudioEncoder(); - if (audioEncoder != null) { - return audioEncoder.getAudioQuality(); - } - } - return null; + protected AudioEncoder getAudioEncoder() { + return mRtmpClient != null ? mRtmpClient.getAudioEncoder() : null; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractRTSPPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractRTSPPreviewServer.java index 25c98723aa..bc9c1a9d70 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractRTSPPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractRTSPPreviewServer.java @@ -1,19 +1,16 @@ package org.deviceconnect.android.deviceplugin.host.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 org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; public abstract class AbstractRTSPPreviewServer extends AbstractPreviewServer { - /** * マイムタイプを定義します. */ @@ -29,17 +26,14 @@ public abstract class AbstractRTSPPreviewServer extends AbstractPreviewServer { */ private RtspServer mRtspServer; - public AbstractRTSPPreviewServer(Context context, HostMediaRecorder recorder) { - this(context, recorder, false); - } - public AbstractRTSPPreviewServer(Context context, HostMediaRecorder recorder, boolean useSSL) { - super(context, recorder, useSSL); + public AbstractRTSPPreviewServer(HostMediaRecorder recorder, String encoderId) { + super(recorder, encoderId); } @Override public String getUri() { - return "rtsp://localhost:" + getPort(); + return "rtsp://localhost:" + getEncoderSettings().getPort(); } @Override @@ -48,57 +42,40 @@ public String getMimeType() { } @Override - public void startWebServer(final OnWebServerStartCallback callback) { + public boolean isRunning() { + return mRtspServer != null; + } + + @Override + public void start(final OnStartCallback callback) { if (mRtspServer == null) { mRtspServer = new RtspServer(); mRtspServer.setServerName(SERVER_NAME); - mRtspServer.setServerPort(getPort()); + mRtspServer.setServerPort(getEncoderSettings().getPort()); mRtspServer.setCallback(mCallback); try { mRtspServer.start(); } catch (Exception e) { - callback.onFail(); + callback.onFailed(e); return; } } - callback.onStart(getUri()); + callback.onSuccess(); } @Override - public void stopWebServer() { + public void stop() { 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); @@ -115,13 +92,13 @@ public void setMute(boolean mute) { } @Override - protected VideoQuality getVideoQuality() { + protected VideoEncoder getVideoEncoder() { if (mRtspServer != null) { RtspSession session = mRtspServer.getRtspSession(); if (session != null) { - VideoStream videoStream = session.getVideoStream(); - if (videoStream != null) { - return videoStream.getVideoEncoder().getVideoQuality(); + VideoStream stream = session.getVideoStream(); + if (stream != null) { + return stream.getVideoEncoder(); } } } @@ -129,13 +106,13 @@ protected VideoQuality getVideoQuality() { } @Override - protected AudioQuality getAudioQuality() { + protected AudioEncoder getAudioEncoder() { if (mRtspServer != null) { RtspSession session = mRtspServer.getRtspSession(); if (session != null) { - AudioStream audioStream = session.getAudioStream(); - if (audioStream != null) { - return audioStream.getAudioEncoder().getAudioQuality(); + AudioStream stream = session.getAudioStream(); + if (stream != null) { + return stream.getAudioEncoder(); } } } @@ -183,7 +160,6 @@ protected VideoStream createVideoStream() { protected AudioStream createAudioStream() { HostMediaRecorder recorder = getRecorder(); HostMediaRecorder.Settings settings = recorder.getSettings(); - if (settings.isAudioEnabled()) { return new MicAACLATMStream(5004); } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractSRTBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractSRTBroadcaster.java index 66df06d39f..2e9b992e41 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractSRTBroadcaster.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractSRTBroadcaster.java @@ -2,14 +2,10 @@ 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 配信クライアント. */ @@ -18,36 +14,19 @@ public abstract class AbstractSRTBroadcaster extends AbstractBroadcaster { /** * イベントを通知するためのリスナー. */ - private OnEventListener mOnBroadcasterEventListener; - - public AbstractSRTBroadcaster(HostMediaRecorder recorder, String broadcastURI) { - super(recorder, broadcastURI); - } + private Broadcaster.OnEventListener mOnBroadcasterEventListener; - /** - * SRT 配信に使用する VideoEncoder のインスタンスを作成します. - * - * @return SRT 配信に使用する VideoEncoder - */ - protected VideoEncoder createVideoEncoder() { - return null; + public AbstractSRTBroadcaster(HostMediaRecorder recorder, String id) { + super(recorder, id); } - /** - * SRT 配信に使用する AudioEncoder のインスタンスを作成します. - * - * @return SRT 配信に使用する AudioEncoder - */ - protected AudioEncoder createAudioEncoder() { - HostMediaRecorder.Settings settings = getRecorder().getSettings(); - if (settings.isAudioEnabled()) { - return new MicAACLATMEncoder(); - } - return null; + @Override + public String getMimeType() { + return "video/MP2T"; } @Override - public void setOnEventListener(OnEventListener listener) { + public void setOnEventListener(Broadcaster.OnEventListener listener) { mOnBroadcasterEventListener = listener; } @@ -58,6 +37,12 @@ public boolean isRunning() { @Override public void start(OnStartCallback callback) { + String broadcastURI = getBroadcastURI(); + if (broadcastURI == null) { + callback.onFailed(new RuntimeException("broadcastURI is not set.")); + return; + } + VideoEncoder videoEncoder = createVideoEncoder(); if (videoEncoder != null) { setVideoQuality(videoEncoder.getVideoQuality()); @@ -68,7 +53,7 @@ public void start(OnStartCallback callback) { setAudioQuality(audioEncoder.getAudioQuality()); } - HostMediaRecorder.Settings settings = getRecorder().getSettings(); + HostMediaRecorder.EncoderSettings settings = getEncoderSettings(); mSrtClient = new SRTClient(getBroadcastURI()); mSrtClient.setMaxRetryCount(settings.getRetryCount()); @@ -132,44 +117,25 @@ public void stop() { @Override public void setMute(boolean mute) { + super.setMute(mute); + if (mSrtClient != null) { mSrtClient.setMute(mute); } } @Override - public boolean isMute() { + public boolean isMuted() { 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; + protected VideoEncoder getVideoEncoder() { + return mSrtClient != null ? mSrtClient.getVideoEncoder() : null; } @Override - protected AudioQuality getAudioQuality() { - if (mSrtClient != null) { - AudioEncoder audioEncoder = mSrtClient.getAudioEncoder(); - if (audioEncoder != null) { - return audioEncoder.getAudioQuality(); - } - } - return null; + protected AudioEncoder getAudioEncoder() { + return mSrtClient != null ? mSrtClient.getAudioEncoder() : null; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractSRTPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractSRTPreviewServer.java index 9aeea91a44..ac726f1615 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractSRTPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/AbstractSRTPreviewServer.java @@ -1,15 +1,11 @@ package org.deviceconnect.android.deviceplugin.host.recorder; -import android.content.Context; import android.util.Log; import org.deviceconnect.android.deviceplugin.host.BuildConfig; -import org.deviceconnect.android.deviceplugin.host.recorder.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; @@ -22,29 +18,18 @@ public abstract class AbstractSRTPreviewServer extends AbstractPreviewServer { */ private static final String MIME_TYPE = "video/MP2T"; - /** - * SRTの設定. - */ - private final SRTSettings mSettings; - /** * SRT サーバ. */ private SRTServer mSRTServer; - public AbstractSRTPreviewServer(Context context, HostMediaRecorder recorder) { - this(context, recorder, false); - } - - public AbstractSRTPreviewServer(Context context, HostMediaRecorder recorder, boolean useSSL) { - super(context, recorder, useSSL); - mSettings = new SRTSettings(context); + public AbstractSRTPreviewServer(HostMediaRecorder recorder, String encoderId) { + super(recorder, encoderId); } - @Override public String getUri() { - return "srt://localhost:" + getPort(); + return "srt://localhost:" + getEncoderSettings().getPort(); } @Override @@ -53,59 +38,43 @@ public String getMimeType() { } @Override - public void startWebServer(final OnWebServerStartCallback callback) { + public boolean isRunning() { + return mSRTServer != null; + } + + @Override + public void start(final OnStartCallback callback) { if (mSRTServer == null) { try { - mSRTServer = new SRTServer(getPort()); + HostMediaRecorder.EncoderSettings settings = getEncoderSettings(); + mSRTServer = new SRTServer(getEncoderSettings().getPort()); mSRTServer.setStatsInterval(BuildConfig.STATS_INTERVAL); mSRTServer.setShowStats(DEBUG); mSRTServer.setCallback(mCallback); - mSRTServer.setSocketOptions(mSettings.loadSRTSocketOptions()); + mSRTServer.setSocketOptions(settings.getSRTSocketOptions()); mSRTServer.start(); } catch (Exception e) { - callback.onFail(); + callback.onFailed(e); return; } } - callback.onStart(getUri()); + callback.onSuccess(); } @Override - public void stopWebServer() { + public void stop() { 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); @@ -122,28 +91,22 @@ public void setMute(boolean mute) { } @Override - protected VideoQuality getVideoQuality() { + protected VideoEncoder getVideoEncoder() { if (mSRTServer != null) { SRTSession session = mSRTServer.getSRTSession(); if (session != null) { - VideoEncoder videoEncoder = session.getVideoEncoder(); - if (videoEncoder != null) { - return videoEncoder.getVideoQuality(); - } + return session.getVideoEncoder(); } } return null; } @Override - protected AudioQuality getAudioQuality() { + protected AudioEncoder getAudioEncoder() { if (mSRTServer != null) { SRTSession session = mSRTServer.getSRTSession(); if (session != null) { - AudioEncoder audioEncoder = session.getAudioEncoder(); - if (audioEncoder != null) { - return audioEncoder.getAudioQuality(); - } + return session.getAudioEncoder(); } } return null; @@ -176,12 +139,26 @@ private void restartAudioEncoder() { /** * SRT 用の映像エンコーダを作成します. * + * null を返却した場合には、映像は配信しません。 + * + * このメソッドを実装することでエンコーダを切り替えます。 + * * @return SRT 用の映像エンコーダ */ protected VideoEncoder createVideoEncoder() { return null; } + /** + * SRT 用の音声エンコーダを作成します. + * + * null を返却した場合には、音声は配信しません。 + * + * デフォルトで、 aac のエンコーダを作成して返却します。 + * aac 以外のエンコーダを実装する場合には、このメソッドをオーバーライドします。 + * + * @return SRT 用の音声エンコーダ + */ protected AudioEncoder createAudioEncoder() { HostMediaRecorder recorder = getRecorder(); HostMediaRecorder.Settings settings = recorder.getSettings(); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/Broadcaster.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/Broadcaster.java index b9a6fdf8ae..08aae3a90d 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/Broadcaster.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/Broadcaster.java @@ -1,100 +1,10 @@ package org.deviceconnect.android.deviceplugin.host.recorder; -public interface Broadcaster { - /** - * マイムタイプを取得します. - * - * @return マイムタイプ - */ - String getMimeType(); - +public interface Broadcaster extends LiveStreaming { /** * ブロードキャスト先の 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/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/BroadcasterProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/BroadcasterProvider.java index 675653085e..a409614bdf 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/BroadcasterProvider.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/BroadcasterProvider.java @@ -1,73 +1,5 @@ package org.deviceconnect.android.deviceplugin.host.recorder; -public interface BroadcasterProvider { - /** - * ブロードキャスターのリストを取得します. - * - * @return ブロードキャスターのリスト - */ - Broadcaster getBroadcaster(); +public interface BroadcasterProvider extends LiveStreamingProvider { - /** - * ブロードキャスト中か確認します. - * - * @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/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/CropInterface.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/CropInterface.java new file mode 100644 index 0000000000..4ca92972d1 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/CropInterface.java @@ -0,0 +1,67 @@ +package org.deviceconnect.android.deviceplugin.host.recorder; + +import android.graphics.Rect; + +public interface CropInterface { + + /** + * PreviewSurfaceView で枠を表示する時に表示する名前を取得します. + * + * @return 名前 + */ + String getName(); + + /** + * start で指定された矩形から end で指定された矩形に移動します. + * + * @param start 開始する矩形 + * @param end 停止する矩形 + * @param duration 移動する時間(ミリ秒) + */ + void moveCropRect(Rect start, Rect end, int duration); + + /** + * クロップ範囲を設定します. + * + * @param rect クロップする範囲の矩形 + */ + void setCropRect(Rect rect); + + /** + * クロップ範囲を取得します. + * + * @return クロップする範囲の矩形 + */ + Rect getCropRect(); + + /** + * イベントリスナーを追加します. + * + * @param listener 追加するリスナー + */ + void addOnEventListener(OnEventListener listener); + + /** + * イベントリスナーを削除します. + * + * @param listener 削除するリスナー + */ + void removeOnEventListener(OnEventListener listener); + + interface OnEventListener { + /** + * クロップする矩形が追加されたことを通知します. + */ + void onAdded(CropInterface crop, Rect cropRect); + + /** + * クロップする矩形が削除されたことを通知します. + */ + void onRemoved(CropInterface crop); + + /** + * クロップする矩形が移動したことを通知します. + */ + void onMoved(CropInterface crop, Rect cropRect); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/HostMediaRecorder.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/HostMediaRecorder.java index e537887342..28ccf450da 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/HostMediaRecorder.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/HostMediaRecorder.java @@ -12,14 +12,19 @@ import android.util.Range; import android.util.Size; +import org.deviceconnect.android.deviceplugin.host.R; import org.deviceconnect.android.deviceplugin.host.recorder.util.CapabilityUtil; import org.deviceconnect.android.deviceplugin.host.recorder.util.MediaProjectionProvider; import org.deviceconnect.android.deviceplugin.host.recorder.util.PropertyUtil; import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; +import org.deviceconnect.android.libsrt.SRT; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import javax.net.ssl.SSLContext; @@ -123,7 +128,7 @@ public interface HostMediaRecorder extends HostDevicePhotoRecorder, HostDeviceSt * * @return 開始したプレビュー配信サーバのリスト */ - List startPreview(); + List startPreview(); /** * プレビュー配信サーバを停止します. @@ -143,7 +148,7 @@ public interface HostMediaRecorder extends HostDevicePhotoRecorder, HostDeviceSt * @param broadcastURI ブロードキャスト先のURI * @return ブロードキャストしているクラス */ - Broadcaster startBroadcaster(String broadcastURI); + List startBroadcaster(String broadcastURI); /** * ブロードキャストを停止します. @@ -183,6 +188,16 @@ public interface HostMediaRecorder extends HostDevicePhotoRecorder, HostDeviceSt */ void requestKeyFrame(); + /** + * ビットレートの更新を要求します. + */ + void requestBitRate(); + + /** + * JPEG 品質の更新を要求します. + */ + void requestJpegQuality(); + /** * ミュート設定を行います. * @@ -240,10 +255,38 @@ interface PermissionCallback { void onDisallowed(); } + enum MimeType { + MJPEG("video/x-mjpeg"), + RTSP("video/x-rtp"), + SRT("video/MP2T"), + RTMP("video/x-rtmp"), + UNKNOWN(""); + + private final String mValue; + + MimeType(String value) { + mValue = value; + } + + public String getValue() { + return mValue; + } + + public static MimeType typeOf(String mimeType) { + for (MimeType type : values()) { + if (type.mValue.equalsIgnoreCase(mimeType)) { + return type; + } + } + return UNKNOWN; + } + } + enum AudioSource { DEFAULT("default"), MIC("mic"), - APP("app"); + APP("app"), + NONE("none"); private final String mSource; @@ -261,18 +304,18 @@ public static AudioSource typeOf(String source) { return audioSource; } } - return null; + return NONE; } } - enum VideoEncoderName { + enum VideoCodec { H264("h264", "video/avc"), H265("h265", "video/hevc"); private final String mName; private final String mMimeType; - VideoEncoderName(String name, String mimeType) { + VideoCodec(String name, String mimeType) { mName = name; mMimeType = mimeType; } @@ -285,8 +328,8 @@ public String getMimeType() { return mMimeType; } - public static VideoEncoderName nameOf(String name) { - for (VideoEncoderName encoder : values()) { + public static VideoCodec nameOf(String name) { + for (VideoCodec encoder : values()) { if (encoder.getName().equalsIgnoreCase(name)) { return encoder; } @@ -335,6 +378,20 @@ public int getProfile() { public int getLevel() { return mLevel; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProfileLevel that = (ProfileLevel) o; + return mProfile == that.mProfile && + mLevel == that.mLevel; + } + + @Override + public int hashCode() { + return Objects.hash(mProfile, mLevel); + } } enum AudioFilter { @@ -412,7 +469,7 @@ interface OnEventListener extends HostDeviceStreamRecorder.OnEventListener, Host * * @param servers 開始したプレビュー配信サーバ */ - void onPreviewStarted(List servers); + void onPreviewStarted(List servers); /** * プレビュー配信を停止した時に呼び出されます. @@ -429,16 +486,14 @@ interface OnEventListener extends HostDeviceStreamRecorder.OnEventListener, Host /** * ブロードキャストを開始した時に呼び出されます. * - * @param broadcaster 開始したブロードキャスト + * @param broadcasters 開始したブロードキャスト */ - void onBroadcasterStarted(Broadcaster broadcaster); + void onBroadcasterStarted(List broadcasters); /** * ブロードキャストを停止した時に呼び出されます. - * - * @param broadcaster 停止したブロードキャスト */ - void onBroadcasterStopped(Broadcaster broadcaster); + void onBroadcasterStopped(); /** * ブロードキャストでエラーが発生したときに呼び出されます. @@ -446,7 +501,7 @@ interface OnEventListener extends HostDeviceStreamRecorder.OnEventListener, Host * @param broadcaster エラーが発生した Broadcaster * @param e エラー原因の例外 */ - void onBroadcasterError(Broadcaster broadcaster, Exception e); + void onBroadcasterError(LiveStreaming broadcaster, Exception e); /** * レコーダで発生したエラーを通知します. @@ -456,68 +511,120 @@ interface OnEventListener extends HostDeviceStreamRecorder.OnEventListener, Host void onError(Exception e); } - /** - * HostMediaRecorder の設定を保持するクラス. - */ - abstract class Settings { - private final PropertyUtil mPref; + class EncoderSettings { + private static final int DEFAULT_PREVIEW_MAX_FRAME_RATE = 30; + private static final int DEFAULT_PREVIEW_BITRATE = 2 * 1024 * 1024; + private static final String DEFAULT_PREVIEW_ENCODER = VideoCodec.H264.mName; + private static final int DEFAULT_PREVIEW_KEY_FRAME_INTERVAL = 1; - public Settings(Context context, HostMediaRecorder recorder) { - mPref = new PropertyUtil(context, recorder.getId().replaceAll("/", "_")); + private final PropertyUtil mProperty; + private final Context mContext; + + public EncoderSettings(Context context, String name) { + mContext = context; + mProperty = new PropertyUtil(context, name); } /** - * 初期化されているか確認します. + * データを削除します. + */ + public void clear() { + mProperty.clear(); + } + + /** + * 名前を取得します. * - * @return 初期化されている場合はtrue、それ以外はfalse + * @return 名前 */ - public boolean isInitialized() { - return mPref.getString("test", null) != null; + public String getName() { + return mProperty.getString("name", null); } /** - * 初期化完了を書き込みます. + * 名前を設定します. + * + * @param name 名前 */ - public void finishInitialization() { - mPref.put("test", "test"); + public void setName(String name) { + mProperty.put("name", name); } /** - * 保存データを初期化します. + * マイムタイプを取得します. + * + * @return マイムタイプ */ - public void clear() { - mPref.clear(); + public MimeType getMimeType() { + return MimeType.typeOf(mProperty.getString("mime_type", null)); } /** - * 自動フォーカスモードを取得します. + * マイムタイプを設定します. * - * 未設定の場合は null を返却します。 + * @param mimeType マイムタイプ + */ + public void setMimeType(MimeType mimeType) { + if (mimeType == null) { + throw new IllegalArgumentException("mimeType is null."); + } + mProperty.put("mime_type", mimeType.getValue()); + } + + /** + * サーバ用のポート番号を取得します. * - * @return 自動フォーカスモード + * @return サーバ用のポート番号 */ - public Integer getPreviewAutoFocusMode() { - return mPref.getInteger("preview_auto_focus", null); + public Integer getPort() { + return mProperty.getInteger("port", 0); } /** - * 自動フォーカスモードを設定します. + * サーバ用のポート番号を設定します. * - * mode が設定された場合には、未設定にします。 + * @param port サーバ用のポート番号 + */ + public void setPort(int port) { + mProperty.put("port", port); + } + + /** + * SSL を使用するか確認します. * - * サポートされていないモードが設定された場合には例外が発生します。 + * @return SSL を使用する場合はtrue、それ以外はfalse + */ + public boolean isUseSSL() { + return mProperty.getBoolean("use_ssl", false); + } + + /** + * SSL 使用フラグを設定します. * - * @param mode 自動フォーカスモード + * @param useSSL SSL 使用フラグ */ - public void setPreviewAutoFocusMode(Integer mode) { - if (mode == null) { - mPref.remove("preview_auto_focus"); - } else { - if (!isSupportedAutoFocusMode(mode)) { - throw new IllegalArgumentException("focus mode is not supported."); - } - mPref.put("preview_auto_focus", mode); - } + public void setUseSSL(boolean useSSL) { + mProperty.put("use_ssl", useSSL); + } + + //// MediaCodec + + /** + * プレビューサイズを取得します. + * + * @return プレビューサイズ + */ + public Size getPreviewSize() { + return mProperty.getSize("preview_size_width", "preview_size_height"); + } + + /** + * プレビューサイズを設定します. + * + * @param previewSize プレビューサイズ + */ + public void setPreviewSize(Size previewSize) { + mProperty.put("preview_size_width", "preview_size_height", previewSize); } /** @@ -525,8 +632,8 @@ public void setPreviewAutoFocusMode(Integer mode) { * * @return エンコード名 */ - public VideoEncoderName getPreviewEncoderName() { - return VideoEncoderName.nameOf(getPreviewEncoder()); + public VideoCodec getPreviewEncoderName() { + return VideoCodec.nameOf(getPreviewEncoder()); } /** @@ -537,7 +644,7 @@ public VideoEncoderName getPreviewEncoderName() { * @return プレビューの配信エンコードの名前 */ public String getPreviewEncoder() { - return mPref.getString("preview_encoder", "h264"); + return mProperty.getString("preview_encoder", DEFAULT_PREVIEW_ENCODER); } /** @@ -547,12 +654,12 @@ public String getPreviewEncoder() { */ public void setPreviewEncoder(String encoder) { if (encoder == null) { - mPref.remove("preview_encoder"); + mProperty.remove("preview_encoder"); } else { if (!isSupportedVideoEncoder(encoder)) { throw new IllegalArgumentException("encoder is not supported."); } - mPref.put("preview_encoder", encoder); + mProperty.put("preview_encoder", encoder); } } @@ -564,8 +671,8 @@ public void setPreviewEncoder(String encoder) { * @return プロファイルとレベル */ public ProfileLevel getProfileLevel() { - Integer profile = mPref.getInteger("preview_profile", null); - Integer level = mPref.getInteger("preview_level", null); + Integer profile = mProperty.getInteger("preview_profile", null); + Integer level = mProperty.getInteger("preview_level", null); if (profile != null && level != null) { return new ProfileLevel(profile, level); } @@ -575,7 +682,7 @@ public ProfileLevel getProfileLevel() { /** * プロファイルとレベルを設定します. * - * null が設定された場合には、未設定にします。 + * null が設定された場合には、値を削除して未設定にします。 * * サポートされていないプロファイルとレベルが設定された場合には例外を発生します。 * @@ -583,14 +690,14 @@ public ProfileLevel getProfileLevel() { */ public void setProfileLevel(ProfileLevel pl) { if (pl == null) { - mPref.remove("preview_profile"); - mPref.remove("preview_level"); + mProperty.remove("preview_profile"); + mProperty.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()); + mProperty.put("preview_profile", pl.getProfile()); + mProperty.put("preview_level", pl.getLevel()); } } @@ -600,7 +707,7 @@ public void setProfileLevel(ProfileLevel pl) { * @return プロファイル */ public Integer getProfile() { - return mPref.getInteger("preview_profile", 0); + return mProperty.getInteger("preview_profile", 0); } /** @@ -609,119 +716,169 @@ public Integer getProfile() { * @return レベル */ public Integer getLevel() { - return mPref.getInteger("preview_level", 0); + return mProperty.getInteger("preview_level", 0); } /** - * 写真サイズを取得します. + * フレームレートを取得します. * - * @return 写真サイズ + * @return フレームレート */ - public Size getPictureSize() { - return mPref.getSize("picture_size_width", "picture_size_height"); + public int getPreviewMaxFrameRate() { + return mProperty.getInteger("preview_framerate", DEFAULT_PREVIEW_MAX_FRAME_RATE); } /** - * 写真サイズを設定します. - * - * サポートされていない写真サイズの場合は IllegalArgumentException を発生させます。 + * フレームレートを設定します. * - * @param pictureSize 写真サイズ + * @param previewMaxFrameRate フレームレート */ - public void setPictureSize(Size pictureSize) { - if (!isSupportedPictureSize(pictureSize)) { - throw new IllegalArgumentException("pictureSize is not supported."); + public void setPreviewMaxFrameRate(Integer previewMaxFrameRate) { + if (previewMaxFrameRate <= 0) { + throw new IllegalArgumentException("previewMaxFrameRate is zero or negative."); } - mPref.put( - "picture_size_width", - "picture_size_height", - pictureSize); + mProperty.put("preview_framerate", previewMaxFrameRate); } /** - * プレビューサイズを取得します. + * ビットレートを取得します. * - * @return プレビューサイズ + * @return ビットレート(byte) */ - public Size getPreviewSize() { - return mPref.getSize("preview_size_width", "preview_size_height"); + public int getPreviewBitRate() { + return mProperty.getInteger("preview_bitrate", DEFAULT_PREVIEW_BITRATE); } /** - * プレビューサイズを設定します. + * ビットレートを設定します. * - * サポートされていないプレビューサイズの場合は IllegalArgumentException を発生させます。 + * @param previewBitRate ビットレート(byte) + */ + public void setPreviewBitRate(int previewBitRate) { + if (previewBitRate <= 0) { + throw new IllegalArgumentException("previewBitRate is zero or negative."); + } + mProperty.put("preview_bitrate", String.valueOf(previewBitRate)); + } + + /** + * ビットレートモードを取得します. * - * @param previewSize プレビューサイズ + * @return ビットレートモード */ - public void setPreviewSize(Size previewSize) { - if (!isSupportedPreviewSize(previewSize)) { - throw new IllegalArgumentException("previewSize is not supported."); + public BitRateMode getPreviewBitRateMode() { + return BitRateMode.nameOf(mProperty.getString("preview_bitrate_mode", null)); + } + + /** + * ビットレートモードを設定します. + * + * @param mode ビットレートモード + */ + public void setPreviewBitRateMode(BitRateMode mode) { + if (mode == null) { + mProperty.remove("preview_bitrate_mode"); + } else { + mProperty.put("preview_bitrate_mode", mode.getName()); } - mPref.put("preview_size_width", "preview_size_height", previewSize); } /** - * フレームレートを取得します. + * キーフレームインターバルを取得します. * - * @return フレームレート + * @return キーフレームを発行する間隔(ミリ秒) */ - public int getPreviewMaxFrameRate() { - return mPref.getInteger("preview_framerate", 30); + public int getPreviewKeyFrameInterval() { + return mProperty.getInteger("preview_i_frame_interval", DEFAULT_PREVIEW_KEY_FRAME_INTERVAL); } /** - * フレームレートを設定します. + * キーフレームインターバルを設定します. * - * @param previewMaxFrameRate フレームレート + * @param previewKeyFrameInterval キーフレームを発行する間隔(ミリ秒) */ - public void setPreviewMaxFrameRate(Integer previewMaxFrameRate) { - if (previewMaxFrameRate <= 0) { - throw new IllegalArgumentException("previewMaxFrameRate is zero or negative."); + public void setPreviewKeyFrameInterval(int previewKeyFrameInterval) { + if (previewKeyFrameInterval < 0) { + throw new IllegalArgumentException("previewKeyFrameInterval is negative."); } - mPref.put("preview_framerate", previewMaxFrameRate); + mProperty.put("preview_i_frame_interval", previewKeyFrameInterval); } /** - * ビットレートを取得します. + * ソフトウェアエンコーダを優先的に使用するフラグを確認します. * - * @return ビットレート(byte) + * @return ソフトウェアエンコーダを優先的に使用する場合は true、それ以外は false */ - public int getPreviewBitRate() { - return mPref.getInteger("preview_bitrate", 2 * 1024 * 1024); + public boolean isUseSoftwareEncoder() { + return mProperty.getBoolean("preview_use_software_encoder", false); } /** - * ビットレートを設定します. + * ソフトウェアエンコーダを優先的に使用するフラグを設定します. * - * @param previewBitRate ビットレート(byte) + * @param used ソフトウェアエンコーダを優先的に使用する場合は true、それ以外は false */ - public void setPreviewBitRate(int previewBitRate) { - if (previewBitRate <= 0) { - throw new IllegalArgumentException("previewBitRate is zero or negative."); + public void setUseSoftwareEncoder(boolean used) { + mProperty.put("preview_use_software_encoder", used); + } + + /** + * イントラリフレッシュのフレーム数を取得します. + * + * @return イントラリフレッシュのフレーム数 + */ + public Integer getIntraRefresh() { + return mProperty.getInteger("preview_intra_refresh", 0); + } + + /** + * イントラリフレッシュのフレーム数を設定します. + * + * @param refresh イントラリフレッシュのフレーム数 + */ + public void setIntraRefresh(Integer refresh) { + if (refresh == null) { + mProperty.remove("preview_intra_refresh"); + } else { + mProperty.put("preview_intra_refresh", refresh); } - mPref.put("preview_bitrate", String.valueOf(previewBitRate)); } /** - * キーフレームインターバルを取得します. + * 切り抜き範囲を取得します. * - * @return キーフレームを発行する間隔(ミリ秒) + * 範囲ば設定されていない場合には、null を返却します. + * + * @return 切り抜き範囲 */ - public int getPreviewKeyFrameInterval() { - return mPref.getInteger("preview_i_frame_interval", 1); + public Rect getCropRect() { + return mProperty.getRect("preview_clip_left", + "preview_clip_top", + "preview_clip_right", + "preview_clip_bottom"); } /** - * キーフレームインターバルを設定します. + * 切り抜き範囲を設定します. * - * @param previewKeyFrameInterval キーフレームを発行する間隔(ミリ秒) + * 引数に null が指定された場合には、切り抜き範囲を削除します。 + * + * @param rect 切り抜き範囲 */ - public void setPreviewKeyFrameInterval(int previewKeyFrameInterval) { - if (previewKeyFrameInterval <= 0) { - throw new IllegalArgumentException("previewKeyFrameInterval is zero or negative."); + public void setCropRect(Rect rect) { + if (rect == null) { + mProperty.remove("preview_clip_left"); + mProperty.remove("preview_clip_top"); + mProperty.remove("preview_clip_right"); + mProperty.remove("preview_clip_bottom"); + } else { + mProperty.put( + "preview_clip_left", + "preview_clip_top", + "preview_clip_right", + "preview_clip_bottom", + rect); } - mPref.put("preview_i_frame_interval", previewKeyFrameInterval); } /** @@ -730,7 +887,7 @@ public void setPreviewKeyFrameInterval(int previewKeyFrameInterval) { * @return プレビューの品質 */ public int getPreviewQuality() { - return mPref.getInteger("preview_jpeg_quality", 80); + return mProperty.getInteger("preview_jpeg_quality", 80); } /** @@ -748,86 +905,579 @@ public void setPreviewQuality(int quality) { if (quality > 100) { throw new IllegalArgumentException("quality is over 100."); } - mPref.put("preview_jpeg_quality", quality); + mProperty.put("preview_jpeg_quality", quality); } /** - * ホワイトバランスの設定を取得します. + * サポートしているエンコーダの解像度の最大値を取得します. * - * @return ホワイトバランス + * @return サポートしている解像度の最大値 */ - public Integer getPreviewWhiteBalance() { - return mPref.getInteger("preview_white_balance", null); + public Size getSupportedPreviewSize() { + VideoCodec codec = getPreviewEncoderName(); + if (codec != null) { + return CapabilityUtil.getSupportedMaxSize(codec.getMimeType()); + } + return CapabilityUtil.getSupportedMaxSize(VideoCodec.H264.getMimeType()); } /** - * ホワイトバランスを設定します. + * サポートしているエンコーダの解像度の最大値を取得します. * - * @param whiteBalance ホワイトバランス + * @param mimeType マイムタイプ + * @return サポートしている解像度の最大値 */ - public void setPreviewWhiteBalance(Integer whiteBalance) { - if (whiteBalance == null) { - mPref.remove("preview_white_balance"); - } else { - if (!isSupportedWhiteBalanceMode(whiteBalance)) { - throw new IllegalArgumentException("WhiteBalance is unsupported value."); - } - mPref.put("preview_white_balance", whiteBalance); - } + public Size getSupportedPreviewSize(String mimeType) { + return CapabilityUtil.getSupportedMaxSize(mimeType); } - public Integer getPreviewAutoExposureMode() { - return mPref.getInteger("preview_auto_exposure_mode", null); + /** + * 指定されたサイズがサポートされているか確認します. + * + * @param mimeType マイムタイプ + * @param size 解像度 + * @return サポートされている場合はtrue、それ以外はfalse + */ + public boolean isSupportedPreviewSize(String mimeType, Size size) { + Size maxSize = getSupportedPreviewSize(mimeType); + return maxSize != null && (size.getWidth() <= maxSize.getWidth() && size.getHeight() <= maxSize.getHeight()); } - public void setPreviewAutoExposureMode(Integer mode) { - if (mode == null) { - mPref.remove("preview_auto_exposure_mode"); - } else { - if (!isSupportedAutoExposureMode(mode)) { - throw new IllegalArgumentException("Exposure mode is unsupported value."); - } - mPref.put("preview_auto_exposure_mode", mode); - } + /** + * 画面に表示するプレビューのリストを取得します. + * + * @return プレビューリスト + */ + public List getSupportedEncoderSizes() { + return new ArrayList<>(); } /** - * ソフトウェアエンコーダを優先的に使用するフラグを確認します. + * サポートしているエンコーダのリストを取得します. * - * @return ソフトウェアエンコーダを優先的に使用する場合は true、それ以外は false + * @return サポートしているエンコーダのリスト */ - public boolean isUseSoftwareEncoder() { - return mPref.getBoolean("preview_use_software_encoder", false); + public List getSupportedVideoEncoders() { + List list = new ArrayList<>(); + List supported = CapabilityUtil.getSupportedVideoEncoders(); + for (VideoCodec encoderName : VideoCodec.values()) { + if (supported.contains(encoderName.getMimeType())) { + list.add(encoderName.getName()); + } + } + return list; } /** - * ソフトウェアエンコーダを優先的に使用するフラグを設定します. + * サポートしているプロファイル・レベルの一覧を取得します. * - * @param used ソフトウェアエンコーダを優先的に使用する場合は true、それ以外は false + * @return サポートしているプロファイル・レベルの一覧 */ - public void setUseSoftwareEncoder(boolean used) { - mPref.put("preview_use_software_encoder", used); + public List getSupportedProfileLevel() { + VideoCodec encoderName = getPreviewEncoderName(); + return CapabilityUtil.getSupportedProfileLevel(encoderName.getMimeType()); + } + + /** + * 指定されたエンコーダがサポートされているか確認します. + * + * @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; + } + + + /** + * SRT サーバに対して設定するオプションの一覧を作成します. + * + * @return オプションの一覧 + */ + public Map getSRTSocketOptions() { + Map options = new HashMap<>(); + for (SRTOptionItem item : SRT_OPTION_ITEMS) { + String key = mContext.getString(item.getPrefKey()); + String value = mProperty.getString(key, null); + if (value == null || "".equals(value)) { + continue; + } + + try { + if (item.getValueClass() == Long.class) { + options.put(item.getOptionEnum(), Long.parseLong(value)); + } else if (item.getValueClass() == Integer.class) { + options.put(item.getOptionEnum(), Integer.parseInt(value)); + } else { + options.put(item.getOptionEnum(), value); + } + } catch (Exception ignored) {} + } + return options; + } + + /** + * 設定画面でサポートする SRT オプションの定義. + */ + private static final List SRT_OPTION_ITEMS = Arrays.asList( + new SRTOptionItem(SRT.SRTO_PEERLATENCY, Integer.class, R.string.pref_key_settings_srt_peerlatency), + new SRTOptionItem(SRT.SRTO_LOSSMAXTTL, Integer.class, R.string.pref_key_settings_srt_lossmaxttl), + new SRTOptionItem(SRT.SRTO_INPUTBW, Long.class, R.string.pref_key_settings_srt_inputbw), + new SRTOptionItem(SRT.SRTO_OHEADBW, Integer.class, R.string.pref_key_settings_srt_oheadbw), + new SRTOptionItem(SRT.SRTO_CONNTIMEO, Integer.class, R.string.pref_key_settings_srt_conntimeo), + new SRTOptionItem(SRT.SRTO_PEERIDLETIMEO, Integer.class, R.string.pref_key_settings_srt_peeridletimeo), + new SRTOptionItem(SRT.SRTO_PACKETFILTER, String.class, R.string.pref_key_settings_srt_packetfilter)); + + /** + * SRT オプション設定項目の定義. + * + * SRT オプションの列挙子 ({@link SRT} で定義されているもの) に対して、値の型とプリファレンスキーを対応づける. + */ + private static 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; + } + + int getPrefKey() { + return mPrefKey; + } + + Class getValueClass() { + return mValueClass; + } + } + + // 配信 + + /** + * 配信先の URI を取得します. + * + * 設定されていない場合は null を返却します. + * + * @return 配信先の URI + */ + public String getBroadcastURI() { + return mProperty.getString("broadcast_uri", null); + } + + /** + * 配信先の URI を設定します. + * + * @param broadcastURI 配信先の URI + */ + public void setBroadcastURI(String broadcastURI) { + mProperty.put("broadcast_uri", broadcastURI); + } + + /** + * リトライ回数を取得します. + * + * @return リトライ回数 + */ + public int getRetryCount() { + return mProperty.getInteger("broadcast_retry_count", 0); + } + + /** + * リトライ回数を設定します. + * + * @param count リトライ回数 + */ + public void setRetryCount(int count) { + if (count < 0) { + mProperty.remove("broadcast_retry_count"); + } else { + mProperty.put("broadcast_retry_count", count); + } + } + + /** + * リトライのインターバルを取得します. + * + * @return リトライのインターバル + */ + public int getRetryInterval() { + return mProperty.getInteger("broadcast_retry_interval", 3000); + } + + /** + * リトライのインターバルを設定します. + * + * @param interval リトライのインターバル + */ + public void setRetryInterval(int interval) { + if (interval < 0) { + mProperty.remove("broadcast_retry_interval"); + } else { + mProperty.put("broadcast_retry_interval", interval); + } + } + } + + /** + * HostMediaRecorder の設定を保持するクラス. + */ + abstract class Settings { + private final PropertyUtil mProperty; + private final Context mContext; + + public Settings(Context context, HostMediaRecorder recorder) { + mContext = context; + mProperty = new PropertyUtil(context, recorder.getId()); + } + + protected EncoderSettings createEncoderSettings(String encoderId) { + return new EncoderSettings(mContext, encoderId); + } + + /** + * 初期化されているか確認します. + * + * @return 初期化されている場合はtrue、それ以外はfalse + */ + public boolean isInitialized() { + return mProperty.getString("initialization", null) != null; + } + + /** + * 初期化完了を書き込みます. + */ + public void finishInitialization() { + mProperty.put("initialization", "completion"); + } + + /** + * 保存データを初期化します. + */ + public void clear() { + for (String encoderId : getEncoderIdList()) { + getEncoderSetting(encoderId).clear(); + } + mProperty.clear(); + } + + /** + * 指定された ID に対応するエンコーダの設定を取得します. + * + * @param encoderId 配信先の設定の ID + * @return エンコーダ設定 + */ + public EncoderSettings getEncoderSetting(String encoderId) { + List encoderSettingList = getEncoderIdList(); + if (encoderSettingList.contains(encoderId)) { + return createEncoderSettings(encoderId); + } + return null; + } + + /** + * エンコーダの ID リストを取得します. + * + * @return エンコーダリスト + */ + public List getEncoderIdList() { + return mProperty.getArrayString("encoder_id_list"); + } + + /** + * エンコーダ ID が存在するか確認します. + * + * @param encoderId エンコーダID + * @return 存在する場合はtrue、それ以外はfalse + */ + public boolean existEncoderId(String encoderId) { + List encoderIdList = getEncoderIdList(); + return encoderIdList.contains(encoderId); + } + + /** + * エンコーダを追加します. + * + * 既に同じ ID が存在する場合には何も処理を行いません。 + * + * @param encoderId 追加するエンコーダ ID + */ + public void addEncoder(String encoderId) { + List encoderList = getEncoderIdList(); + if (encoderList.contains(encoderId)) { + return; + } + encoderList.add(encoderId); + mProperty.put("encoder_id_list", encoderList); + } + + /** + * エンコーダを削除します. + * + * @param encoderId 削除するエンコーダ ID + */ + public void removeEncoder(String encoderId) { + EncoderSettings encoderSettings = getEncoderSetting(encoderId); + if (encoderId != null && encoderSettings != null) { + encoderSettings.clear(); + } + + List encoderList = getEncoderIdList(); + encoderList.remove(encoderId); + mProperty.put("encoder_id_list", encoderList); + } + + // カメラ設定 + + /** + * カメラの向きを取得します. + * + * @return カメラの向き + */ + public int getOrientation() { + return mProperty.getInteger("camera_orientation", -1); + } + + /** + * カメラの向きを設定します. + * + * @param orientation カメラの向き + */ + public void setOrientation(int orientation) { + mProperty.put("camera_orientation", orientation); + } + + /** + * 写真サイズを取得します. + * + * @return 写真サイズ + */ + public Size getPictureSize() { + return mProperty.getSize("picture_size_width", "picture_size_height"); + } + + /** + * 写真サイズを設定します. + * + * サポートされていない写真サイズの場合は IllegalArgumentException を発生させます。 + * + * @param pictureSize 写真サイズ + */ + public void setPictureSize(Size pictureSize) { + if (pictureSize == null) { + throw new IllegalArgumentException("pictureSize is not set."); + } + if (!isSupportedPictureSize(pictureSize)) { + throw new IllegalArgumentException("pictureSize is not supported."); + } + mProperty.put("picture_size_width", "picture_size_height", pictureSize); + } + + /** + * プレビューサイズを取得します. + * + * @return プレビューサイズ + */ + public Size getPreviewSize() { + return mProperty.getSize("preview_size_width", "preview_size_height"); + } + + /** + * プレビューサイズを設定します. + * + * サポートされていないプレビューサイズの場合は IllegalArgumentException を発生させます。 + * + * @param previewSize プレビューサイズ + */ + public void setPreviewSize(Size previewSize) { + if (previewSize == null) { + throw new IllegalArgumentException("previewSize is not set."); + } + if (!isSupportedPreviewSize(previewSize)) { + throw new IllegalArgumentException("previewSize is not supported."); + } + mProperty.put("preview_size_width", "preview_size_height", previewSize); + } + + /** + * 設定が近い fps を取得します. + * + * @param frameRate フレームレート + * @return fps + */ + public Range getPreviewFpsFromFrameRate(int frameRate) { + List> fpsList = getSupportedFps(); + for (Range fps : fpsList) { + if (frameRate == fps.getLower() && frameRate == fps.getUpper()) { + return fps; + } + } + Range t = null; + int diff = Integer.MAX_VALUE; + for (Range fps : fpsList) { + if (fps.getLower() < frameRate && frameRate <= fps.getUpper()) { + if (t != null) { + int a = fps.getUpper() - frameRate; + int b = fps.getLower() - frameRate; + int l = a * a + b * b; + if (l < diff) { + diff = l; + t = fps; + } + } else { + t = fps; + } + } + } + return t; + } + + /** + * フレームレートを取得します. + * + * @return フレームレート + */ + public Range getPreviewFps() { + Integer lower = mProperty.getInteger("preview_fps_lower", null); + Integer upper = mProperty.getInteger("preview_fps_upper", null); + if (lower != null && upper != null) { + return new Range<>(lower, upper); + } + return null; + } + + /** + * フレームレートを設定します. + * + * サポートされていないフレームレート場合は IllegalArgumentException を発生させます。 + * + * @param fps フレームレート + */ + public void setPreviewFps(Range fps) { + if (fps == null) { + mProperty.remove("preview_fps_lower"); + mProperty.remove("preview_fps_upper"); + } else { + if (!isSupportedFps(fps)) { + throw new IllegalArgumentException("previewFps is not supported."); + } + mProperty.put("preview_fps_lower", fps.getLower()); + mProperty.put("preview_fps_upper", fps.getUpper()); + } + } + + /** + * 自動フォーカスモードを取得します. + * + * 未設定の場合は null を返却します。 + * + * @return 自動フォーカスモード + */ + public Integer getPreviewAutoFocusMode() { + return mProperty.getInteger("preview_auto_focus", null); + } + + /** + * 自動フォーカスモードを設定します. + * + * mode が設定された場合には、未設定にします。 + * + * サポートされていないモードが設定された場合には例外が発生します。 + * + * @param mode 自動フォーカスモード + */ + public void setPreviewAutoFocusMode(Integer mode) { + if (mode == null) { + mProperty.remove("preview_auto_focus"); + } else { + if (!isSupportedAutoFocusMode(mode)) { + throw new IllegalArgumentException("focus mode is not supported."); + } + mProperty.put("preview_auto_focus", mode); + } + } + + /** + * ホワイトバランスの設定を取得します. + * + * @return ホワイトバランス + */ + public Integer getPreviewWhiteBalance() { + return mProperty.getInteger("preview_white_balance", null); + } + + /** + * ホワイトバランスを設定します. + * + * @param whiteBalance ホワイトバランス + */ + public void setPreviewWhiteBalance(Integer whiteBalance) { + if (whiteBalance == null) { + mProperty.remove("preview_white_balance"); + } else { + if (!isSupportedWhiteBalanceMode(whiteBalance)) { + throw new IllegalArgumentException("WhiteBalance is unsupported value."); + } + mProperty.put("preview_white_balance", whiteBalance); + } } /** - * イントラリフレッシュのフレーム数を取得します. + * 自動露出モードを取得します. * - * @return イントラリフレッシュのフレーム数 + * @return 自動露出モード */ - public Integer getIntraRefresh() { - return mPref.getInteger("preview_intra_refresh", 0); + public Integer getPreviewAutoExposureMode() { + return mProperty.getInteger("preview_auto_exposure_mode", null); } /** - * イントラリフレッシュのフレーム数を設定します. + * 自動露出モードを設定します. * - * @param refresh イントラリフレッシュのフレーム数 + * mode に null が指定された場合は設定を削除します。 + * + * @param mode 自動露出モード */ - public void setIntraRefresh(Integer refresh) { - if (refresh == null) { - mPref.remove("preview_intra_refresh"); + public void setPreviewAutoExposureMode(Integer mode) { + if (mode == null) { + mProperty.remove("preview_auto_exposure_mode"); } else { - mPref.put("preview_intra_refresh", refresh); + if (!isSupportedAutoExposureMode(mode)) { + throw new IllegalArgumentException("Exposure mode is unsupported value."); + } + mProperty.put("preview_auto_exposure_mode", mode); } } @@ -837,7 +1487,7 @@ public void setIntraRefresh(Integer refresh) { * @return 手ぶれ補正モード */ public Integer getStabilizationMode() { - return mPref.getInteger("preview_stabilization_mode", null); + return mProperty.getInteger("preview_stabilization_mode", null); } /** @@ -847,12 +1497,12 @@ public Integer getStabilizationMode() { */ public void setStabilizationMode(Integer mode) { if (mode == null) { - mPref.remove("preview_stabilization_mode"); + mProperty.remove("preview_stabilization_mode"); } else { if (!isSupportedStabilization(mode)) { throw new IllegalArgumentException("Stabilization Mode is unsupported value."); } - mPref.put("preview_stabilization_mode", mode); + mProperty.put("preview_stabilization_mode", mode); } } @@ -862,7 +1512,7 @@ public void setStabilizationMode(Integer mode) { * @return 光学手ぶれ補正モード */ public Integer getOpticalStabilizationMode() { - return mPref.getInteger("preview_optical_stabilization_mode", null); + return mProperty.getInteger("preview_optical_stabilization_mode", null); } /** @@ -872,12 +1522,12 @@ public Integer getOpticalStabilizationMode() { */ public void setOpticalStabilizationMode(Integer mode) { if (mode == null) { - mPref.remove("preview_optical_stabilization_mode"); + mProperty.remove("preview_optical_stabilization_mode"); } else { if (!isSupportedOpticalStabilization(mode)) { throw new IllegalArgumentException("Optical Stabilization Mode is unsupported value."); } - mPref.put("preview_optical_stabilization_mode", mode); + mProperty.put("preview_optical_stabilization_mode", mode); } } @@ -887,7 +1537,7 @@ public void setOpticalStabilizationMode(Integer mode) { * @return デジタルズーム */ public Float getDigitalZoom() { - return mPref.getFloat("preview_digital_zoom", null); + return mProperty.getFloat("preview_digital_zoom", null); } /** @@ -897,17 +1547,24 @@ public Float getDigitalZoom() { */ public void setDigitalZoom(Float zoom) { if (zoom == null) { - mPref.remove("preview_digital_zoom"); + mProperty.remove("preview_digital_zoom"); } else { if (!isSupportedDigitalZoom(zoom)) { throw new IllegalArgumentException("Digital zoom is unsupported value."); } - mPref.put("preview_digital_zoom", zoom); + mProperty.put("preview_digital_zoom", zoom); } } + /** + * 焦点距離を取得します. + * + * 未設定の場合は null を返却します。 + * + * @return 焦点距離 + */ public Float getFocalLength() { - Float value = mPref.getFloat("preview_focal_length", null); + Float value = mProperty.getFloat("preview_focal_length", null); if (value == null) { return null; } @@ -920,156 +1577,195 @@ public Float getFocalLength() { return null; } + /** + * 焦点距離を設定します. + * + * focalLength に null が指定された場合には、設定を削除します。 + * サポートされていない値が指定された場合には、IllegalArgumentException が発生します。 + * + * @param focalLength 焦点距離 + */ public void setFocalLength(Float focalLength) { if (focalLength == null) { - mPref.remove("preview_focal_length"); + mProperty.remove("preview_focal_length"); } else { if (!isSupportedFocalLength(focalLength)) { throw new IllegalArgumentException("focalLength cannot set."); } - mPref.put("preview_focal_length", focalLength); + mProperty.put("preview_focal_length", focalLength); } } + /** + * ノイズ低減モードを取得します. + * + * 未設定の場合は、null を返却します。 + * + * @return ノイズ低減モード + */ public Integer getNoiseReduction() { - return mPref.getInteger("preview_reduction_noise", null); + return mProperty.getInteger("preview_reduction_noise", null); } + /** + * ノイズ低減モードを設定します. + * + * mode に null が指定された場合には、設定を削除します。 + * サポートされていない値が指定された場合には、IllegalArgumentException が発生します。 + * + * @param mode ノイズ低減モード + */ public void setNoiseReduction(Integer mode) { if (mode == null) { - mPref.remove("preview_reduction_noise"); + mProperty.remove("preview_reduction_noise"); } else { if (!isSupportedNoiseReduction(mode)) { throw new IllegalArgumentException("mode cannot set."); } - mPref.put("preview_reduction_noise", mode); - } - } - - public BitRateMode getPreviewBitRateMode() { - return BitRateMode.nameOf(mPref.getString("preview_bitrate_mode", null)); - } - - public void setPreviewBitRateMode(BitRateMode mode) { - if (mode == null) { - mPref.remove("preview_bitrate_mode"); - } else { - mPref.put("preview_bitrate_mode", mode.getName()); + mProperty.put("preview_reduction_noise", mode); } } + /** + * 自動露出モードを取得します. + * + * @return 自動露出モード + */ public Integer getAutoExposureMode() { - return mPref.getInteger("preview_auto_exposure_mode", null); + return mProperty.getInteger("preview_auto_exposure_mode", null); } + /** + * 自動露出モードを設定します. + * + * mode に null が指定された場合には設定を削除します。 + * サポートされていない値が指定された場合には、IllegalArgumentException が発生します。 + * + * @param mode 自動露出モード + */ public void setAutoExposureMode(Integer mode) { if (mode == null) { - mPref.remove("preview_auto_exposure_mode"); + mProperty.remove("preview_auto_exposure_mode"); } else { if (!isSupportedAutoExposureMode(mode)) { throw new IllegalArgumentException("mode cannot set."); } - mPref.put("preview_auto_exposure_mode", mode); + mProperty.put("preview_auto_exposure_mode", mode); } } + /** + * 露出時間を取得します. + * + * @return 露出時間 + */ public Long getSensorExposureTime() { - return mPref.getLong("preview_sensor_exposure_time", null); + return mProperty.getLong("preview_sensor_exposure_time", null); } + /** + * 露出時間を設定します. + * + * mode に null が指定された場合には設定を削除します。 + * サポートされていない値が指定された場合には、IllegalArgumentException が発生します。 + * + * @param exposureTime 露出時間 + */ public void setSensorExposureTime(Long exposureTime) { if (exposureTime == null) { - mPref.remove("preview_sensor_exposure_time"); + mProperty.remove("preview_sensor_exposure_time"); } else { if (!isSupportedSensorExposureTime(exposureTime)) { throw new IllegalArgumentException("exposureTime cannot set."); } - mPref.put("preview_sensor_exposure_time", exposureTime); + mProperty.put("preview_sensor_exposure_time", exposureTime); } } + /** + * ISO 感度を取得します. + * + * @return ISO 感度 + */ public Integer getSensorSensitivity() { - return mPref.getInteger("preview_sensor_sensitivity", null); + return mProperty.getInteger("preview_sensor_sensitivity", null); } + /** + * ISO 感度を設定します. + * + * mode に null が指定された場合には設定を削除します。 + * サポートされていない値が指定された場合には、IllegalArgumentException が発生します。 + * + * @param sensitivity ISO 感度 + */ public void setSensorSensitivity(Integer sensitivity) { if (sensitivity == null) { - mPref.remove("preview_sensor_sensitivity"); + mProperty.remove("preview_sensor_sensitivity"); } else { - if (!isSupportedSensorSensorSensitivity(sensitivity)) { + if (!isSupportedSensorSensitivity(sensitivity)) { throw new IllegalArgumentException("sensitivity cannot set."); } - mPref.put("preview_sensor_sensitivity", sensitivity); + mProperty.put("preview_sensor_sensitivity", sensitivity); } } + /** + * フレーム時間を取得します. + * + * 未設定の場合は null を返却します。 + * + * @return フレーム時間 + */ public Long getSensorFrameDuration() { - return mPref.getLong("preview_sensor_frame_duration", null); + return mProperty.getLong("preview_sensor_frame_duration", null); } + /** + * フレーム時間を設定します. + * + * @param frameDuration フレーム時間 + */ public void setSensorFrameDuration(Long frameDuration) { if (frameDuration == null) { - mPref.remove("preview_sensor_frame_duration"); + mProperty.remove("preview_sensor_frame_duration"); } else { if (!isSupportedSensorFrameDuration(frameDuration)) { throw new IllegalArgumentException("frameDuration cannot set."); } - mPref.put("preview_sensor_frame_duration", frameDuration); - } - } - - public Integer getPreviewWhiteBalanceTemperature() { - return mPref.getInteger("preview_sensor_white_balance_temperature", null); - } - - public void setPreviewWhiteBalanceTemperature(Integer temperature) { - if (temperature == null) { - mPref.remove("preview_sensor_white_balance_temperature"); - } else { - if (!isSupportedWhiteBalanceTemperature(temperature)) { - throw new IllegalArgumentException("whiteBalanceTemperature cannot set."); - } - mPref.put("preview_sensor_white_balance_temperature", temperature); + mProperty.put("preview_sensor_frame_duration", frameDuration); } } /** - * 切り抜き範囲を取得します. + * 色温度を取得します. * - * 範囲ば設定されていない場合には、null を返却します. + * 未設定の場合は null を返却します。 * - * @return 切り抜き範囲 + * @return 色温度 */ - public Rect getDrawingRange() { - return mPref.getRect("preview_clip_left", - "preview_clip_top", - "preview_clip_right", - "preview_clip_bottom"); + public Integer getPreviewWhiteBalanceTemperature() { + return mProperty.getInteger("preview_sensor_white_balance_temperature", null); } /** - * 切り抜き範囲を設定します. + * 色温度を設定します. * - * 引数に null が指定された場合には、切り抜き範囲を削除します。 - * - * @param rect 切り抜き範囲 + * @param temperature 色温度 */ - 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"); + public void setPreviewWhiteBalanceTemperature(Integer temperature) { + if (temperature == null) { + mProperty.remove("preview_sensor_white_balance_temperature"); } else { - mPref.put( - "preview_clip_left", - "preview_clip_top", - "preview_clip_right", - "preview_clip_bottom", - rect); + if (!isSupportedWhiteBalanceTemperature(temperature)) { + throw new IllegalArgumentException("whiteBalanceTemperature cannot set."); + } + mProperty.put("preview_sensor_white_balance_temperature", temperature); } } + /// サポートしているデータサイズ + /** * サポートしている写真サイズを取得します. * @@ -1180,6 +1876,13 @@ public Long getMaxSensorFrameDuration() { return null; } + /** + * サポートしている色温度の範囲を取得します. + * + * サポートしていない場合は null を返却します。 + * + * @return 色温度の範囲 + */ public Range getSupportedWhiteBalanceTemperature() { return null; } @@ -1192,7 +1895,7 @@ public Range getSupportedWhiteBalanceTemperature() { public List getSupportedVideoEncoders() { List list = new ArrayList<>(); List supported = CapabilityUtil.getSupportedVideoEncoders(); - for (VideoEncoderName encoderName : VideoEncoderName.values()) { + for (VideoCodec encoderName : VideoCodec.values()) { if (supported.contains(encoderName.getMimeType())) { list.add(encoderName.getName()); } @@ -1200,23 +1903,55 @@ public List getSupportedVideoEncoders() { return list; } - public List getSupportedProfileLevel() { - VideoEncoderName encoderName = getPreviewEncoderName(); + /** + * サポートしているプロファイル・レベルの一覧を取得します. + * + * @return サポートしているプロファイル・レベルの一覧 + */ + public List getSupportedProfileLevel(VideoCodec encoderName) { return CapabilityUtil.getSupportedProfileLevel(encoderName.getMimeType()); } + /** + * サポートしている手ぶれ補正のリストを取得します. + * + * サポートしていない場合は、空のリストを返却します。 + * + * @return サポートしている手ぶれ補正のリスト + */ public List getSupportedStabilizationList() { return new ArrayList<>(); } + /** + * サポートしている光学手ぶれ補正のリストを取得します. + * + * サポートしていない場合は、空のリストを返却します。 + * + * @return サポートしている光学手ぶれ補正のリスト + */ public List getSupportedOpticalStabilizationList() { return new ArrayList<>(); } + /** + * サポートしているノイズ低減モートのリストを取得します. + * + * サポートしていない場合は、空のリストを返却します。 + * + * @return サポートしているノイズ低減モートのリスト + */ public List getSupportedNoiseReductionList() { return new ArrayList<>(); } + /** + * サポートしている焦点距離のリストを取得します. + * + * サポートしていない場合は、空のリストを返却します。 + * + * @return サポートしている焦点距離のリスト + */ public List getSupportedFocalLengthList() { return new ArrayList<>(); } @@ -1302,6 +2037,12 @@ public boolean isSupportedFps(Range fps) { return false; } + /** + * 指定されたフォーカスモードがサポートされているか確認します. + * + * @param mode 確認するフォーカスモード + * @return サポートされている場合はtrue、それ以外はfalse + */ public boolean isSupportedAutoFocusMode(int mode) { List modeList = getSupportedAutoFocusModeList(); if (modeList != null) { @@ -1332,6 +2073,12 @@ public boolean isSupportedWhiteBalanceMode(int whiteBalance) { return false; } + /** + * 指定された色温度がサポートされているか確認します. + * + * @param temperature 色温度 + * @return サポートされている場合はtrue、それ以外はfalse + */ public boolean isSupportedWhiteBalanceTemperature(int temperature) { Range range = getSupportedWhiteBalanceTemperature(); if (range != null) { @@ -1340,6 +2087,12 @@ public boolean isSupportedWhiteBalanceTemperature(int temperature) { return false; } + /** + * 指定された自動露出モードがサポートされているか確認します. + * + * @param mode 自動露出モード + * @return サポートされている場合はtrue、それ以外はfalse + */ public boolean isSupportedAutoExposureMode(Integer mode) { List modeList = getSupportedAutoExposureModeList(); if (modeList != null) { @@ -1352,6 +2105,12 @@ public boolean isSupportedAutoExposureMode(Integer mode) { return false; } + /** + * 指定された露出時間がサポートされているか確認します. + * + * @param time 露出時間 + * @return サポートされている場合はtrue、それ以外はfalse + */ public boolean isSupportedSensorExposureTime(long time) { Range range = getSupportedSensorExposureTime(); if (range != null) { @@ -1360,7 +2119,13 @@ public boolean isSupportedSensorExposureTime(long time) { return false; } - public boolean isSupportedSensorSensorSensitivity(int sensitivity) { + /** + * 指定された ISO 感度がサポートされているか確認します. + * + * @param sensitivity ISO 感度 + * @return サポートされている場合はtrue、それ以外はfalse + */ + public boolean isSupportedSensorSensitivity(int sensitivity) { Range range = getSupportedSensorSensitivity(); if (range != null) { return range.getLower() <= sensitivity && sensitivity <= range.getUpper(); @@ -1368,6 +2133,12 @@ public boolean isSupportedSensorSensorSensitivity(int sensitivity) { return false; } + /** + * 指定されたフレーム時間がサポートされているか確認します. + * + * @param frameDuration フレーム時間 + * @return サポートされている場合はtrue、それ以外はfalse + */ public boolean isSupportedSensorFrameDuration(long frameDuration) { Long maxFrameDuration = getMaxSensorFrameDuration(); if (maxFrameDuration != null) { @@ -1397,12 +2168,13 @@ public boolean isSupportedVideoEncoder(String encoder) { /** * 指定されたプロファイルとレベルがサポートされているか確認します. * + * @param codec コーデック * @param profile プロファイル * @param level レベル * @return サポートされている場合はtrue、それ以外はfalse */ - public boolean isSupportedProfileLevel(int profile, int level) { - List list = getSupportedProfileLevel(); + public boolean isSupportedProfileLevel(VideoCodec codec, int profile, int level) { + List list = getSupportedProfileLevel(codec); if (list != null) { for (ProfileLevel pl : list) { if (profile == pl.getProfile() && level == pl.getLevel()) { @@ -1504,7 +2276,7 @@ public boolean isSupportedFocalLength(Float focalLength) { * @return プレビュー音声が有効の場合はtrue、それ以外はfalse */ public boolean isAudioEnabled() { - return getPreviewAudioSource() != null; + return getPreviewAudioSource() != AudioSource.NONE; } /** @@ -1513,7 +2285,7 @@ public boolean isAudioEnabled() { * @return 音声タイプ */ public AudioSource getPreviewAudioSource() { - return AudioSource.typeOf(mPref.getString("preview_audio_source", "none")); + return AudioSource.typeOf(mProperty.getString("preview_audio_source", "none")); } /** @@ -1523,9 +2295,9 @@ public AudioSource getPreviewAudioSource() { */ public void setPreviewAudioSource(AudioSource audioSource) { if (audioSource == null) { - mPref.put("preview_audio_source", "none"); + mProperty.put("preview_audio_source", "none"); } else { - mPref.put("preview_audio_source", audioSource.mSource); + mProperty.put("preview_audio_source", audioSource.mSource); } } @@ -1535,7 +2307,7 @@ public void setPreviewAudioSource(AudioSource audioSource) { * @return プレビュー音声のビットレート */ public int getPreviewAudioBitRate() { - return mPref.getInteger("preview_audio_bitrate", 64 * 1024); + return mProperty.getInteger("preview_audio_bitrate", 64 * 1024); } /** @@ -1547,7 +2319,7 @@ public void setPreviewAudioBitRate(int bitRate) { if (bitRate <= 0) { throw new IllegalArgumentException("previewAudioBitRate is zero or negative value."); } - mPref.put("preview_audio_bitrate", bitRate); + mProperty.put("preview_audio_bitrate", bitRate); } /** @@ -1556,7 +2328,7 @@ public void setPreviewAudioBitRate(int bitRate) { * @return プレビュー音声のサンプルレート */ public int getPreviewSampleRate() { - return mPref.getInteger("preview_audio_sample_rate", 16000); + return mProperty.getInteger("preview_audio_sample_rate", 16000); } /** @@ -1566,12 +2338,12 @@ public int getPreviewSampleRate() { */ public void setPreviewSampleRate(Integer sampleRate) { if (sampleRate == null) { - mPref.remove("preview_audio_sample_rate"); + mProperty.remove("preview_audio_sample_rate"); } else { if (!isSupportedSampleRate(sampleRate)) { throw new IllegalArgumentException("preivewSampleRate is invalid."); } - mPref.put("preview_audio_sample_rate", sampleRate); + mProperty.put("preview_audio_sample_rate", sampleRate); } } @@ -1581,7 +2353,7 @@ public void setPreviewSampleRate(Integer sampleRate) { * @return プレビュー音声のチャンネル数 */ public int getPreviewChannel() { - return mPref.getInteger("preview_audio_channel", 1); + return mProperty.getInteger("preview_audio_channel", 1); } /** @@ -1590,7 +2362,7 @@ public int getPreviewChannel() { * @param channel プレビュー音声のチャンネル数 */ public void setPreviewChannel(int channel) { - mPref.put("preview_audio_channel", channel); + mProperty.put("preview_audio_channel", channel); } /** @@ -1599,7 +2371,7 @@ public void setPreviewChannel(int channel) { * @return プレビュー配信のエコーキャンセラー */ public boolean isUseAEC() { - return mPref.getBoolean("preview_audio_aec", true); + return mProperty.getBoolean("preview_audio_aec", true); } /** @@ -1608,7 +2380,7 @@ public boolean isUseAEC() { * @param used プレビュー配信のエコーキャンセラー */ public void setUseAEC(boolean used) { - mPref.put("preview_audio_aec", used); + mProperty.put("preview_audio_aec", used); } /** @@ -1617,7 +2389,7 @@ public void setUseAEC(boolean used) { * @return ミュートの場合はtrue、それ以外の場合はfalse */ public boolean isMute() { - return mPref.getBoolean("preview_audio_mute", false); + return mProperty.getBoolean("preview_audio_mute", false); } /** @@ -1626,27 +2398,27 @@ public boolean isMute() { * @param mute ミュートにする場合はtrue、それ以外はfalse */ public void setMute(boolean mute) { - mPref.put("preview_audio_mute", mute); + mProperty.put("preview_audio_mute", mute); } public AudioFilter getAudioFilter() { - return AudioFilter.nameOf(mPref.getString("preview_audio_filter", "none")); + return AudioFilter.nameOf(mProperty.getString("preview_audio_filter", "none")); } public void setAudioFilter(AudioFilter filter) { if (filter == null) { - mPref.remove("preview_audio_filter"); + mProperty.remove("preview_audio_filter"); } else { - mPref.put("preview_audio_filter", filter.mName); + mProperty.put("preview_audio_filter", filter.mName); } } public float getAudioCoefficient() { - return mPref.getInteger("preview_audio_coefficient", 10) / 100.0f; + return mProperty.getInteger("preview_audio_coefficient", 10) / 100.0f; } public void setAudioCoefficient(float coefficient) { - mPref.put("preview_audio_coefficient", (int) (coefficient * 100)); + mProperty.put("preview_audio_coefficient", (int) (coefficient * 100)); } public boolean isSupportedAudioSource(AudioSource source) { @@ -1706,145 +2478,5 @@ public boolean isSupportedSampleRate(int sampleRate) { } return false; } - - // 配信 - - /** - * 配信先の 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/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/HostMediaRecorderManager.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/HostMediaRecorderManager.java index 5cf671c121..600f27734d 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/HostMediaRecorderManager.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/HostMediaRecorderManager.java @@ -118,7 +118,7 @@ public HostMediaRecorderManager(final DevicePluginContext pluginContext, final F mHostDevicePluginContext = pluginContext; mMediaProjectionProvider = new MediaProjectionProvider(pluginContext.getContext()); mFileManager = fileManager; - initRecorders(); + createRecorders(); } private Context getContext() { @@ -126,13 +126,13 @@ private Context getContext() { } /** - * レコーダの初期化処理を行います. + * 使用するレコーダの一覧を作成します. * *

* ここで使用できるレコーダの登録を行います。 *

*/ - private void initRecorders() { + private void createRecorders() { if (checkCameraHardware(getContext())) { try { mCameraWrapperManager = new CameraWrapperManager(getContext()); @@ -157,80 +157,6 @@ private void initRecorders() { // ignore. } } - - for (HostMediaRecorder recorder : mRecorders) { - recorder.setOnEventListener(new HostMediaRecorder.OnEventListener() { - @Override - public void onMuteChanged(boolean mute) { - postOnMuteChanged(recorder, mute); - } - - @Override - public void onConfigChanged() { - postOnConfigChanged(recorder); - } - - @Override - public void onPreviewStarted(List servers) { - postOnPreviewStarted(recorder, servers); - } - - @Override - public void onPreviewStopped() { - postOnPreviewStopped(recorder); - } - - @Override - public void onPreviewError(Exception e) { - postOnPreviewError(recorder, e); - } - - @Override - public void onBroadcasterStarted(Broadcaster broadcaster) { - postOnBroadcasterStarted(recorder, broadcaster); - } - - @Override - public void onBroadcasterStopped(Broadcaster broadcaster) { - postOnBroadcasterStopped(recorder, broadcaster); - } - - @Override - public void onBroadcasterError(Broadcaster broadcaster, Exception e) { - postOnBroadcasterError(recorder, broadcaster, e); - } - - @Override - public void onError(Exception e) { - postOnError(recorder, e); - } - - @Override - public void onTakePhoto(String uri, String filePath, String mimeType) { - postOnTakePhoto(recorder, uri, filePath, mimeType); - } - - @Override - public void onRecordingStarted(String fileName) { - postOnRecordingStarted(recorder, fileName); - } - - @Override - public void onRecordingPause() { - postOnRecordingPause(recorder); - } - - @Override - public void onRecordingResume() { - postOnRecordingResume(recorder); - } - - @Override - public void onRecordingStopped(String fileName) { - postOnRecordingStopped(recorder, fileName); - } - }); - } } /** @@ -270,12 +196,92 @@ private void createCameraRecorders(final CameraWrapperManager cameraMgr, final F } } + /** + * レコーダの初期化処理を行います. + * + * @param recorder 初期化するレコーダ + */ + private void initRecorder(HostMediaRecorder recorder) { + recorder.setOnEventListener(new HostMediaRecorder.OnEventListener() { + @Override + public void onMuteChanged(boolean mute) { + postOnMuteChanged(recorder, mute); + } + + @Override + public void onConfigChanged() { + postOnConfigChanged(recorder); + } + + @Override + public void onPreviewStarted(List servers) { + postOnPreviewStarted(recorder, servers); + } + + @Override + public void onPreviewStopped() { + postOnPreviewStopped(recorder); + } + + @Override + public void onPreviewError(Exception e) { + postOnPreviewError(recorder, e); + } + + @Override + public void onBroadcasterStarted(List broadcasters) { + postOnBroadcasterStarted(recorder, broadcasters); + } + + @Override + public void onBroadcasterStopped() { + postOnBroadcasterStopped(recorder); + } + + @Override + public void onBroadcasterError(LiveStreaming broadcaster, Exception e) { + postOnBroadcasterError(recorder, broadcaster, e); + } + + @Override + public void onError(Exception e) { + postOnError(recorder, e); + } + + @Override + public void onTakePhoto(String uri, String filePath, String mimeType) { + postOnTakePhoto(recorder, uri, filePath, mimeType); + } + + @Override + public void onRecordingStarted(String fileName) { + postOnRecordingStarted(recorder, fileName); + } + + @Override + public void onRecordingPause() { + postOnRecordingPause(recorder); + } + + @Override + public void onRecordingResume() { + postOnRecordingResume(recorder); + } + + @Override + public void onRecordingStopped(String fileName) { + postOnRecordingStopped(recorder, fileName); + } + }); + recorder.initialize(); + } + /** * 初期化処理を行います. */ public void initialize() { for (HostMediaRecorder recorder : getRecorders()) { - recorder.initialize(); + initRecorder(recorder); } onDisplayRotationChanged(getContext()); @@ -318,14 +324,70 @@ public void destroy() { mCameraWrapperManager.destroy(); } + /** + * レコーダが存在するか確認します. + * + * @param cameraId カメラID + * @return 存在する場合は true、それ以外は false + */ + private boolean existRecorder(String cameraId) { + for (HostMediaRecorder recorder : mRecorders) { + if (recorder instanceof Camera2Recorder) { + String id = ((Camera2Recorder) recorder).getCameraWrapper().getId(); + if (cameraId.equals(id)) { + return true; + } + } + } + return false; + } + + /** + * ロストしたカメラを一旦レコーダから削除します. + */ + private void removeLostCamera() { + List removeList = new ArrayList<>(); + for (HostMediaRecorder recorder : mRecorders) { + if (recorder instanceof Camera2Recorder) { + String cameraId = ((Camera2Recorder) recorder).getCameraWrapper().getId(); + if (mCameraWrapperManager.getCameraById(cameraId) == null) { + removeList.add((Camera2Recorder) recorder); + } + } + } + + for (Camera2Recorder recorder : removeList) { + recorder.destroy(); + mRecorders.remove(recorder); + postOnLostRecorder(recorder); + } + } + /** * 端末が対応しているレコーダを読み込みし直す */ public void reloadRecorders() { - destroy(); - mRecorders.clear(); - initRecorders(); + try { + mCameraWrapperManager.reload(); + + removeLostCamera(); + + for (CameraWrapper camera : mCameraWrapperManager.getCameraList()) { + if (existRecorder(camera.getId())) { + continue; + } + + Camera2Recorder recorder = new Camera2Recorder(getContext(), camera, mFileManager, mMediaProjectionProvider); + initRecorder(recorder); + mRecorders.add(recorder); + + postOnFoundRecorder(recorder); + } + } catch (Exception e) { + // ignore. + } } + /** * 指定されたレコーダが使用できるか確認します. * @@ -418,6 +480,8 @@ public List getCameraRecorders() { * @return レコーダ */ public HostMediaRecorder getRecorder(final String id) { + reloadRecorders(); + if (mRecorders.size() == 0) { return null; } @@ -592,7 +656,7 @@ private void postOnConfigChanged(HostMediaRecorder recorder) { } } - private void postOnPreviewStarted(HostMediaRecorder recorder, List servers) { + private void postOnPreviewStarted(HostMediaRecorder recorder, List servers) { for (OnEventListener l : mOnEventListeners) { l.onPreviewStarted(recorder, servers); } @@ -610,19 +674,19 @@ private void postOnPreviewError(HostMediaRecorder recorder, Exception e) { } } - private void postOnBroadcasterStarted(HostMediaRecorder recorder, Broadcaster broadcaster) { + private void postOnBroadcasterStarted(HostMediaRecorder recorder, List broadcasters) { for (OnEventListener l : mOnEventListeners) { - l.onBroadcasterStarted(recorder, broadcaster); + l.onBroadcasterStarted(recorder, broadcasters); } } - private void postOnBroadcasterStopped(HostMediaRecorder recorder, Broadcaster broadcaster) { + private void postOnBroadcasterStopped(HostMediaRecorder recorder) { for (OnEventListener l : mOnEventListeners) { - l.onBroadcasterStopped(recorder, broadcaster); + l.onBroadcasterStopped(recorder); } } - private void postOnBroadcasterError(HostMediaRecorder recorder, Broadcaster broadcaster, Exception e) { + private void postOnBroadcasterError(HostMediaRecorder recorder, LiveStreaming broadcaster, Exception e) { for (OnEventListener l : mOnEventListeners) { l.onBroadcasterError(recorder, broadcaster, e); } @@ -658,6 +722,26 @@ private void postOnRecordingStopped(HostMediaRecorder recorder, String fileName) } } + private void postOnFoundRecorder(HostMediaRecorder recorder) { + for (OnEventListener l : mOnEventListeners) { + try { + l.onFoundRecorder(recorder); + } catch (Exception e) { + // ignore. + } + } + } + + private void postOnLostRecorder(HostMediaRecorder recorder) { + for (OnEventListener l : mOnEventListeners) { + try { + l.onLostRecorder(recorder); + } catch (Exception e) { + // ignore. + } + } + } + private void postOnError(HostMediaRecorder recorder, Exception e) { for (OnEventListener l : mOnEventListeners) { l.onError(recorder, e); @@ -668,13 +752,13 @@ public interface OnEventListener { void onMuteChanged(HostMediaRecorder recorder, boolean mute); void onConfigChanged(HostMediaRecorder recorder); - void onPreviewStarted(HostMediaRecorder recorder, List servers); + void onPreviewStarted(HostMediaRecorder recorder, List servers); void onPreviewStopped(HostMediaRecorder recorder); void onPreviewError(HostMediaRecorder recorder, Exception e); - void onBroadcasterStarted(HostMediaRecorder recorder, Broadcaster broadcaster); - void onBroadcasterStopped(HostMediaRecorder recorder, Broadcaster broadcaster); - void onBroadcasterError(HostMediaRecorder recorder, Broadcaster broadcaster, Exception e); + void onBroadcasterStarted(HostMediaRecorder recorder, List broadcasters); + void onBroadcasterStopped(HostMediaRecorder recorder); + void onBroadcasterError(HostMediaRecorder recorder, LiveStreaming broadcaster, Exception e); void onTakePhoto(HostMediaRecorder recorder, String uri, String filePath, String mimeType); @@ -683,6 +767,9 @@ public interface OnEventListener { void onRecordingResume(HostMediaRecorder recorder); void onRecordingStopped(HostMediaRecorder recorder, String fileName); + void onFoundRecorder(HostMediaRecorder recorder); + void onLostRecorder(HostMediaRecorder recorder); + void onError(HostMediaRecorder recorder, Exception e); } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/LiveStreaming.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/LiveStreaming.java new file mode 100644 index 0000000000..858dbd5d92 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/LiveStreaming.java @@ -0,0 +1,162 @@ +package org.deviceconnect.android.deviceplugin.host.recorder; + +import javax.net.ssl.SSLContext; + +public interface LiveStreaming { + /** + * サーバのIDします. + * + * @return サーバID + */ + String getId(); + + /** + * サーバが配信するプレビューのマイムタイプを取得します. + * + * @return マイムタイプ + */ + String getMimeType(); + + /** + * サーバへの URL を取得します. + * + * @return サーバへの URL + */ + String getUri(); + + /** + * 起動中か確認します. + * + * @return 起動中の場合は true、それ以外は false + */ + boolean isRunning(); + + /** + * サーバを開始します. + * + * @param callback 開始結果を通知するコールバック + */ + void start(OnStartCallback callback); + + /** + * サーバを停止します. + */ + void stop(); + + /** + * 設定が変更されたことを通知します. + */ + void onConfigChange(); + + /** + * Recorder をミュート状態にする. + */ + void setMute(boolean mute); + + /** + * Recorder のミュート状態を返す. + * @return mute状態 + */ + boolean isMuted(); + + /** + * 映像のエンコーダーに対して sync frame の即時生成を要求する. + * + * @return 即時生成を受け付けた場合はtrue, そうでない場合はfalse + */ + boolean requestSyncFrame(); + + /** + * 映像のエンコーダーにビットレートの更新を要求する. + * + * @return 更新を受け付けた場合はtrue, そうでない場合はfalse + */ + boolean requestBitRate(); + + /** + * JPEG 品質の更新を要求する. + * + * @return 更新を受け付けた場合はtrue, そうでない場合はfalse + */ + boolean requestJpegQuality(); + + /** + * プレビューサーバから配信したデータの BPS を取得します. + * + * @return プレビューサーバから配信したデータの BPS + */ + long getBPS(); + + /** + * サーバを解放します. + */ + void release(); + + /** + * イベントを通知するリスナーを設定します. + * + * @param listener リスナー + */ + void setOnEventListener(OnEventListener listener); + + /** + * SSLContext を使用するかどうかのフラグを返す. + * + * @return SSLContext を使用する場合はtrue, そうでない場合はfalse + */ + boolean useSSLContext(); + + /** + * SSL コンテキストの設定を行います. + * + * @param sslContext SSL コンテキスト + */ + void setSSLContext(SSLContext sslContext); + + /** + * SSL コンテキストを取得します. + * + * @return SSL コンテキスト + */ + SSLContext getSSLContext(); + + /** + * 起動結果を通知するコールバック. + */ + 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/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/LiveStreamingProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/LiveStreamingProvider.java new file mode 100644 index 0000000000..55f2bc5c3b --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/LiveStreamingProvider.java @@ -0,0 +1,124 @@ +package org.deviceconnect.android.deviceplugin.host.recorder; + +import java.util.List; + +public interface LiveStreamingProvider { + /** + * 配信用のサーバを作成します. + * + * @param encoderId サーバの識別子 + * @param encoderSettings エンコード設定 + * @return サーバ + */ + LiveStreaming createLiveStreaming(String encoderId, HostMediaRecorder.EncoderSettings encoderSettings); + + /** + * 追加します. + * + * @param liveStreaming 追加するプレビュー配信サーバ + */ + void addLiveStreaming(LiveStreaming liveStreaming); + + /** + * 削除します. + * + * @param encoderId サーバの識別子 + */ + void removeLiveStreaming(String encoderId); + + /** + * サポートしているプレビュー配信用サーバのリストを取得します. + * @return プレビュー配信用サーバのリスト + */ + List getLiveStreamingList(); + + /** + * プレビューで配信するマイムタイプを取得します. + * + * @return プレビューで配信するマイムタイプ + */ + List getSupportedMimeType(); + + /** + * プレビューサーバが動作している確認します. + * + * @return 動作中の場合は true、それ以外は false + */ + boolean isRunning(); + + /** + * 全てのプレビュー配信サーバを開始します. + * + * レスポンスのリストが空の場合には、全てのプレビュー配信サーバの起動に失敗しています。 + * + * @return 起動に成功したプレビュー配信サーバのリスト + */ + List start(); + + /** + * 全てのプレビュー配信サーバを停止します. + */ + void stop(); + + /** + * 映像のエンコーダーに対して sync frame の即時生成を要求する. + */ + void requestSyncFrame(); + + /** + * 映像のエンコーダーに対してビットレートの更新を要求する。 + */ + void requestBitRate(); + + /** + * 映像のエンコーダーに対して JPEG の品質の更新を要求する。 + */ + void requestJpegQuality(); + + /** + * 設定が変更されたことを通知します. + */ + void onConfigChange(); + + /** + * Recorder をミュート状態にする. + */ + void setMute(boolean mute); + + /** + * イベントを通知するリスナーを設定します. + * + * @param listener リスナー + */ + void setOnEventListener(OnEventListener listener); + + /** + * 配信用のサーバを開放します. + */ + void release(); + + /** + * プレビュー配信サーバのイベントを通知するリスナー. + */ + interface OnEventListener { + /** + * プレビュー配信サーバを開始したことを通知します. + * + * @param servers 開始したサーバのリスト + */ + void onStarted(List servers); + + /** + * プレビュー配信サーバを停止したことを通知します. + */ + void onStopped(); + + /** + * プレビュー配信サーバでエラーが発生したことを通知します. + * + * @param server エラーが発生したサーバ + * @param e エラー原因の例外 + */ + void onError(LiveStreaming server, Exception e); + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/PreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/PreviewServer.java index 7dc5f8289d..8daa347c4a 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/PreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/PreviewServer.java @@ -6,117 +6,9 @@ */ package org.deviceconnect.android.deviceplugin.host.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); +public interface PreviewServer extends LiveStreaming { - /** - * Called when a web server failed to start. - */ - void onFail(); - } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/PreviewServerProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/PreviewServerProvider.java index c8d63fdeae..a37d3133df 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/PreviewServerProvider.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/PreviewServerProvider.java @@ -6,108 +6,6 @@ */ package org.deviceconnect.android.deviceplugin.host.recorder; -import java.util.List; +public interface PreviewServerProvider extends LiveStreamingProvider { -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/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioBroadcasterProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioBroadcasterProvider.java index 488d9ab72a..7655c04596 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioBroadcasterProvider.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioBroadcasterProvider.java @@ -3,25 +3,20 @@ import android.content.Context; import org.deviceconnect.android.deviceplugin.host.recorder.AbstractBroadcastProvider; -import org.deviceconnect.android.deviceplugin.host.recorder.Broadcaster; +import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; +import org.deviceconnect.android.deviceplugin.host.recorder.LiveStreaming; public class AudioBroadcasterProvider extends AbstractBroadcastProvider { - private final HostAudioRecorder mRecorder; - public AudioBroadcasterProvider(Context context, HostAudioRecorder recorder) { super(context, recorder); - mRecorder = recorder; } @Override - public Broadcaster createBroadcaster(String broadcastURI) { - if (broadcastURI.startsWith("srt://")) { - return new AudioSRTBroadcaster(mRecorder, broadcastURI); - } else if (broadcastURI.startsWith("rtmp://") || broadcastURI.startsWith("rtmps://")) { - return new AudioRTMPBroadcaster(mRecorder, broadcastURI); - } else { - return null; + public LiveStreaming createLiveStreaming(String encoderId, HostMediaRecorder.EncoderSettings encoderSettings) { + if (encoderSettings.getMimeType() == HostMediaRecorder.MimeType.RTMP) { + return new AudioRTMPBroadcaster((HostAudioRecorder) getRecorder(), encoderId); } + return null; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioConst.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioConst.java deleted file mode 100755 index b703559e5e..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioConst.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - AudioConst.java - Copyright (c) 2014 NTT DOCOMO,INC. - Released under the MIT license - http://opensource.org/licenses/mit-license.php - */ - -package org.deviceconnect.android.deviceplugin.host.recorder.audio; - -/** - * 音声録音Broadcastで使用する定数を定義. - * - * [音声録音開始リクエストBroadcast] - * ・ホストデバイスプラグインのHostMediaStreamingRecordingProfileから送信される。 - * ・action: SEND_HOSTDP_TO_AUDIO - * ・putExtra(EXTRA_NAME, EXTRA_NAME_AUDIO_RECORD_START); - * - * [音声録音停止リクエストBroadcast] - * ・ホストデバイスプラグインのHostMediaStreamingRecordingProfileへレスポンスを返す。 - * ・action: SEND_CAMERA_TO_AUDIO - * ・putExtra(EXTRA_NAME, EXTRA_NAME_AUDIO_RECORD_STOP); - * - * [音声録音一時停止リクエストBroadcast] - * ・ホストデバイスプラグインのHostMediaStreamingRecordingProfileへレスポンスを返す。 - * ・action: SEND_CAMERA_TO_AUDIO - * ・putExtra(EXTRA_NAME, EXTRA_NAME_AUDIO_RECORD_PAUSE); - * - * @author NTT DOCOMO, INC. - */ -public final class AudioConst { - - /** - * Constructor. - */ - private AudioConst() { - // No operation. - } - - /** Audio呼び出しアクション. */ - public static final String SEND_HOSTDP_TO_AUDIO = - "org.deviceconnect.android.intent.action.SEND_HOSTDP_TO_AUDIO"; - - /** コマンド名. */ - public static final String EXTRA_NAME = "command"; - - /** 再生. */ - public static final String EXTRA_NAME_AUDIO_RECORD_START = "start"; - - /** 停止. */ - public static final String EXTRA_NAME_AUDIO_RECORD_STOP = "stop"; - - /** 一時停止. */ - public static final String EXTRA_NAME_AUDIO_RECORD_PAUSE = "pause"; - - /** Resume. */ - public static final String EXTRA_NAME_AUDIO_RECORD_RESUME = "resume"; - /** ServiceのID. */ - public static final String EXTRA_SERVICE_ID = "serviceId"; - /** 使用するレコーダーのID. */ - public static final String EXTRA_RECORDER_ID = "recorderId"; - - /** ファイル名. */ - public static final String EXTRA_FILE_NAME = "filename"; - - /** コールバック */ - public static final String EXTRA_CALLBACK = "callback"; - - /** コールバックのエラーメッセージ。 */ - public static final String EXTRA_CALLBACK_ERROR_MESSAGE = "callback_error_message"; - - /** フォーマット名. */ - public static final String FORMAT_TYPE = ".aac"; -} diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioPreviewServer.java deleted file mode 100644 index bb0a0dc497..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioPreviewServer.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.deviceconnect.android.deviceplugin.host.recorder.audio; - -import android.content.Context; - -import org.deviceconnect.android.deviceplugin.host.recorder.AbstractPreviewServer; -import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; - -public class AudioPreviewServer extends AbstractPreviewServer { - /** - * コンストラクタ. - * - * @param context コンテキスト - * @param recorder プレビューで表示するレコーダ - */ - public AudioPreviewServer(Context context, HostMediaRecorder recorder) { - super(context, recorder); - } - - @Override - public String getMimeType() { - return null; - } - - @Override - public String getUri() { - return null; - } - - @Override - public void startWebServer(OnWebServerStartCallback callback) { - } - - @Override - public void stopWebServer() { - } - - @Override - public boolean requestSyncFrame() { - return false; - } - - @Override - public long getBPS() { - return 0; - } -} diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioPreviewServerProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioPreviewServerProvider.java index 5c39dc7213..5e373f1fa7 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioPreviewServerProvider.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioPreviewServerProvider.java @@ -4,6 +4,7 @@ import org.deviceconnect.android.deviceplugin.host.recorder.AbstractPreviewServerProvider; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; +import org.deviceconnect.android.deviceplugin.host.recorder.LiveStreaming; public class AudioPreviewServerProvider extends AbstractPreviewServerProvider { /** @@ -14,10 +15,16 @@ public class AudioPreviewServerProvider extends AbstractPreviewServerProvider { */ public AudioPreviewServerProvider(Context context, HostMediaRecorder recorder) { super(context, recorder); + } - HostMediaRecorder.Settings settings = recorder.getSettings(); - - addServer(new AudioRTSPPreviewServer(context, recorder, settings.getRtspPort())); - addServer(new AudioSRTPreviewServer(context, recorder, settings.getSrtPort())); + @Override + public LiveStreaming createLiveStreaming(String encoderId, HostMediaRecorder.EncoderSettings encoderSettings) { + switch (encoderSettings.getMimeType()) { + case RTSP: + return new AudioRTSPPreviewServer(getRecorder(), encoderId); + case SRT: + return new AudioSRTPreviewServer(getRecorder(), encoderId); + } + return null; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioRTMPBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioRTMPBroadcaster.java index ac0eaf6a0f..ac4525746b 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioRTMPBroadcaster.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioRTMPBroadcaster.java @@ -4,13 +4,15 @@ import android.graphics.Color; import org.deviceconnect.android.deviceplugin.host.recorder.AbstractRTMPBroadcaster; +import org.deviceconnect.android.deviceplugin.host.recorder.Broadcaster; import org.deviceconnect.android.libmedia.streaming.video.CanvasVideoEncoder; import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; public class AudioRTMPBroadcaster extends AbstractRTMPBroadcaster { private final HostAudioRecorder mRecorder; - public AudioRTMPBroadcaster(HostAudioRecorder recorder, String broadcastURI) { - super(recorder, broadcastURI); + + public AudioRTMPBroadcaster(HostAudioRecorder recorder, String encoderId) { + super(recorder, encoderId); mRecorder = recorder; } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioRTSPPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioRTSPPreviewServer.java index 60d3b6880f..2f1b147e8c 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioRTSPPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioRTSPPreviewServer.java @@ -1,13 +1,10 @@ package org.deviceconnect.android.deviceplugin.host.recorder.audio; -import android.content.Context; - import org.deviceconnect.android.deviceplugin.host.recorder.AbstractRTSPPreviewServer; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; public class AudioRTSPPreviewServer extends AbstractRTSPPreviewServer { - AudioRTSPPreviewServer(Context context, HostMediaRecorder recorder, int port) { - super(context, recorder); - setPort(port); + AudioRTSPPreviewServer(HostMediaRecorder recorder, String encoderId) { + super(recorder, encoderId); } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioSRTBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioSRTBroadcaster.java index 35e58e4050..0c994d59fd 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioSRTBroadcaster.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioSRTBroadcaster.java @@ -3,7 +3,7 @@ import org.deviceconnect.android.deviceplugin.host.recorder.AbstractSRTBroadcaster; public class AudioSRTBroadcaster extends AbstractSRTBroadcaster { - public AudioSRTBroadcaster(HostAudioRecorder recorder, String broadcastURI) { - super(recorder, broadcastURI); + public AudioSRTBroadcaster(HostAudioRecorder recorder, String name) { + super(recorder, name); } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioSRTPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioSRTPreviewServer.java index eecc2911ef..94429bf564 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioSRTPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/AudioSRTPreviewServer.java @@ -1,13 +1,10 @@ package org.deviceconnect.android.deviceplugin.host.recorder.audio; -import android.content.Context; - import org.deviceconnect.android.deviceplugin.host.recorder.AbstractSRTPreviewServer; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; public class AudioSRTPreviewServer extends AbstractSRTPreviewServer { - AudioSRTPreviewServer(final Context context, final HostMediaRecorder recorder, final int port) { - super(context, recorder); - setPort(port); + AudioSRTPreviewServer(HostMediaRecorder recorder, String encoderId) { + super(recorder, encoderId); } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/HostAudioRecorder.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/HostAudioRecorder.java index 0127b62c19..660d454cb1 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/HostAudioRecorder.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/audio/HostAudioRecorder.java @@ -74,18 +74,44 @@ public HostAudioRecorder(final Context context, FileManager fileManager, MediaPr private void initSettings() { if (!mSettings.isInitialized()) { mSettings.setPreviewSize(new Size(320, 240)); - mSettings.setPreviewBitRate(512 * 1024); - mSettings.setPreviewMaxFrameRate(30); - mSettings.setPreviewKeyFrameInterval(1); + // 音声設定 mSettings.setPreviewAudioSource(AudioSource.DEFAULT); - mSettings.setPreviewAudioBitRate(64 * 1024); - mSettings.setPreviewSampleRate(16000); + mSettings.setPreviewAudioBitRate(128 * 1024); + mSettings.setPreviewSampleRate(48000); mSettings.setPreviewChannel(1); mSettings.setUseAEC(true); - mSettings.setRtspPort(32000); - mSettings.setSrtPort(33000); + // 各サーバ設定 + mSettings.addEncoder(getId() + "-RTSP"); + EncoderSettings rtsp = mSettings.getEncoderSetting(getId() + "-RTSP"); + rtsp.setName("RTSP"); + rtsp.setMimeType(MimeType.RTSP); + rtsp.setPort(32000); + rtsp.setPreviewSize(new Size(320, 240)); + rtsp.setPreviewBitRate(2 * 1024 * 1024); + rtsp.setPreviewMaxFrameRate(30); + rtsp.setPreviewKeyFrameInterval(5); + + mSettings.addEncoder(getId() + "-SRT"); + EncoderSettings srt = mSettings.getEncoderSetting(getId() + "-SRT"); + srt.setName("SRT"); + srt.setMimeType(MimeType.SRT); + srt.setPort(33000); + srt.setPreviewSize(new Size(320, 240)); + srt.setPreviewBitRate(2 * 1024 * 1024); + srt.setPreviewMaxFrameRate(30); + srt.setPreviewKeyFrameInterval(5); + + mSettings.addEncoder(getId() + "-RTMP"); + EncoderSettings rtmp = mSettings.getEncoderSetting(getId() + "-RTMP"); + rtmp.setName("RTMP"); + rtmp.setMimeType(MimeType.RTMP); + rtmp.setPreviewSize(new Size(320, 240)); + rtmp.setPreviewBitRate(2 * 1024 * 1024); + rtmp.setPreviewMaxFrameRate(30); + rtmp.setPreviewKeyFrameInterval(5); + rtmp.setBroadcastURI("rtmp://localhost:1935"); mSettings.finishInitialization(); } @@ -238,7 +264,7 @@ public boolean hasVideo() { // private method. private String generateAudioFileName() { - return "android_audio_" + mSimpleDateFormat.format(new Date()) + AudioConst.FORMAT_TYPE; + return "android_audio_" + mSimpleDateFormat.format(new Date()) + ".aac"; } protected MP4Recorder createMP4Recorder() { diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2BroadcasterProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2BroadcasterProvider.java index 95d167af93..4e31144488 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2BroadcasterProvider.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2BroadcasterProvider.java @@ -3,27 +3,19 @@ import android.content.Context; import org.deviceconnect.android.deviceplugin.host.recorder.AbstractBroadcastProvider; -import org.deviceconnect.android.deviceplugin.host.recorder.Broadcaster; +import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; +import org.deviceconnect.android.deviceplugin.host.recorder.LiveStreaming; public class Camera2BroadcasterProvider extends AbstractBroadcastProvider { - /** - * カメラを操作するレコーダ. - */ - private final Camera2Recorder mRecorder; - public Camera2BroadcasterProvider(Context context, Camera2Recorder recorder) { super(context, recorder); - mRecorder = recorder; } @Override - public Broadcaster createBroadcaster(String broadcastURI) { - if (broadcastURI.startsWith("srt://")) { - return new Camera2SRTBroadcaster(mRecorder, broadcastURI); - } else if (broadcastURI.startsWith("rtmp://") || broadcastURI.startsWith("rtmps://")) { - return new Camera2RTMPBroadcaster(mRecorder, broadcastURI); - } else { - return null; + public LiveStreaming createLiveStreaming(String encoderId, HostMediaRecorder.EncoderSettings encoderSettings) { + if (encoderSettings.getMimeType() == HostMediaRecorder.MimeType.RTMP) { + return new Camera2RTMPBroadcaster((Camera2Recorder) getRecorder(), encoderId); } + return null; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2MJPEGPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2MJPEGPreviewServer.java index a124fae9e7..7071a24e74 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2MJPEGPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2MJPEGPreviewServer.java @@ -6,21 +6,15 @@ */ package org.deviceconnect.android.deviceplugin.host.recorder.camera; -import android.content.Context; -import android.graphics.SurfaceTexture; - import org.deviceconnect.android.deviceplugin.host.recorder.AbstractMJPEGPreviewServer; import org.deviceconnect.android.libmedia.streaming.mjpeg.MJPEGEncoder; /** * カメラのプレビューをMJPEG形式で配信するサーバー. - * - * {@link SurfaceTexture} をもとに実装. */ class Camera2MJPEGPreviewServer extends AbstractMJPEGPreviewServer { - Camera2MJPEGPreviewServer(Context context, Camera2Recorder recorder, int port, boolean useSSL) { - super(context, recorder, useSSL); - setPort(port); + Camera2MJPEGPreviewServer(Camera2Recorder recorder, String encoderId) { + super(recorder, encoderId); } @Override diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2PreviewServerProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2PreviewServerProvider.java index a54dae45a4..035b108f44 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2PreviewServerProvider.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2PreviewServerProvider.java @@ -10,6 +10,7 @@ import org.deviceconnect.android.deviceplugin.host.recorder.AbstractPreviewServerProvider; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; +import org.deviceconnect.android.deviceplugin.host.recorder.LiveStreaming; import org.deviceconnect.android.deviceplugin.host.recorder.PreviewServer; import org.deviceconnect.android.deviceplugin.host.recorder.util.OverlayManager; @@ -34,20 +35,12 @@ class Camera2PreviewServerProvider extends AbstractPreviewServerProvider { */ Camera2PreviewServerProvider(final Context context, final Camera2Recorder recorder) { super(context, recorder); - mOverlayManager = new OverlayManager(context, recorder); - - HostMediaRecorder.Settings settings = recorder.getSettings(); - - addServer(new Camera2MJPEGPreviewServer(context, recorder, settings.getMjpegPort(), false)); - addServer(new Camera2MJPEGPreviewServer(context, recorder, settings.getMjpegSSLPort(), true)); - addServer(new Camera2RTSPPreviewServer(context, recorder, settings.getRtspPort())); - addServer(new Camera2SRTPreviewServer(context, recorder, settings.getSrtPort())); } @Override - public List startServers() { - List servers = super.startServers(); + public List start() { + List servers = super.start(); if (!servers.isEmpty()) { mOverlayManager.registerBroadcastReceiver(); } @@ -55,9 +48,9 @@ public List startServers() { } @Override - public void stopServers() { + public void stop() { mOverlayManager.destroy(); - super.stopServers(); + super.stop(); } @Override @@ -65,4 +58,17 @@ public void onConfigChange() { super.onConfigChange(); mOverlayManager.onConfigChange(); } + + @Override + public LiveStreaming createLiveStreaming(String encoderId, HostMediaRecorder.EncoderSettings encoderSettings) { + switch (encoderSettings.getMimeType()) { + case MJPEG: + return new Camera2MJPEGPreviewServer((Camera2Recorder) getRecorder(), encoderId); + case RTSP: + return new Camera2RTSPPreviewServer((Camera2Recorder) getRecorder(), encoderId); + case SRT: + return new Camera2SRTPreviewServer((Camera2Recorder) getRecorder(), encoderId); + } + return null; + } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2RTMPBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2RTMPBroadcaster.java index 96ab040074..7fad036ed1 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2RTMPBroadcaster.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2RTMPBroadcaster.java @@ -5,8 +5,8 @@ public class Camera2RTMPBroadcaster extends AbstractRTMPBroadcaster { - public Camera2RTMPBroadcaster(Camera2Recorder recorder, String broadcastURI) { - super(recorder, broadcastURI); + public Camera2RTMPBroadcaster(Camera2Recorder recorder, String encoderId) { + super(recorder, encoderId); } @Override diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2RTSPPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2RTSPPreviewServer.java index 5b8f101439..061a512b6a 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2RTSPPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2RTSPPreviewServer.java @@ -1,27 +1,23 @@ package org.deviceconnect.android.deviceplugin.host.recorder.camera; -import android.content.Context; import android.os.Build; import androidx.annotation.RequiresApi; import org.deviceconnect.android.deviceplugin.host.recorder.AbstractRTSPPreviewServer; -import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; import org.deviceconnect.android.libmedia.streaming.rtsp.session.video.VideoStream; @RequiresApi(Build.VERSION_CODES.LOLLIPOP) class Camera2RTSPPreviewServer extends AbstractRTSPPreviewServer { - Camera2RTSPPreviewServer(Context context, Camera2Recorder recorder, int port) { - super(context, recorder); - setPort(port); + Camera2RTSPPreviewServer(Camera2Recorder recorder, String encoderId) { + super(recorder, encoderId); } @Override protected VideoStream createVideoStream() { Camera2Recorder recorder = (Camera2Recorder) getRecorder(); - HostMediaRecorder.Settings settings = recorder.getSettings(); - switch (settings.getPreviewEncoderName()) { + switch (getEncoderSettings().getPreviewEncoderName()) { case H264: default: return new CameraH264VideoStream(recorder, 5006); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2Recorder.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2Recorder.java index 4ea30ff09c..db69ceede8 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2Recorder.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2Recorder.java @@ -39,11 +39,35 @@ import java.io.File; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Locale; public class Camera2Recorder extends AbstractMediaRecorder { + protected static final List ENCODE_SIZE_LIST = Arrays.asList( + new Size(128, 96), + new Size(320, 240), + new Size(640, 480), + new Size(800, 600), + new Size(1024, 768), + new Size(1280, 960), + new Size(1440, 1080), + new Size(1600, 1200), + new Size(320, 180), + new Size(640, 360), + new Size(854, 480), + new Size(1280, 720), + new Size(1366, 768), + new Size(1920, 1080), + new Size(2560, 1440), + new Size(3840, 2160), + new Size(5760, 3240), + new Size(7680, 4320) + ); + /** * カメラターゲットIDの定義. */ @@ -150,49 +174,96 @@ public Camera2Recorder(Context context, CameraWrapper camera, FileManager fileMa mCamera2BroadcasterProvider = new Camera2BroadcasterProvider(context, this); } + /** + * サイズの小さい方からソートを行うための比較演算子. + */ + 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()); + }; + + private List getEncoderSizeList() { + Size maxSize = CapabilityUtil.getSupportedMaxSize(VideoCodec.H264.getMimeType()); + List sizes = new ArrayList<>(); + for (Size size : ENCODE_SIZE_LIST) { + if (size.getWidth() <= maxSize.getWidth() && size.getHeight() <= maxSize.getHeight()) { + sizes.add(size); + } + } + Collections.sort(sizes, SIZE_COMPARATOR); + return sizes; + } + /** * レコーダの設定を初期化します. */ private void initSupportedSettings() { CameraWrapper.Options options = mCameraWrapper.getOptions(); - // MediaCodec でエンコードできる最大解像度を取得 - // TODO h264, h265 で最大解像度が違う場合はどうするべきか? - // TODO ハードウェアエンコーダとソフトウェアエンコーダで最大解像度が違うのはどうするべきか? - Size maxSize = CapabilityUtil.getSupportedMaxSize("video/avc"); - List supportPreviewSizes = new ArrayList<>(); - for (Size size : options.getSupportedPreviewSizeList()) { - if (maxSize != null) { - if (size.getWidth() <= maxSize.getWidth() && size.getHeight() <= maxSize.getHeight()) { - supportPreviewSizes.add(size); - } - } - } - + List> supportedFpsList = options.getSupportedFpsList(); mSettings.mSupportedPictureSize = new ArrayList<>(options.getSupportedPictureSizeList()); - mSettings.mSupportedPreviewSize = supportPreviewSizes; + mSettings.mSupportedPreviewSize = new ArrayList<>(options.getSupportedPreviewSizeList()); + mSettings.mSupportedEncoderSize = getEncoderSizeList(); if (!mSettings.isInitialized()) { + // カメラ設定 mSettings.setPictureSize(options.getDefaultPictureSize()); mSettings.setPreviewSize(options.getDefaultPreviewSize()); - mSettings.setPreviewBitRate(2 * 1024 * 1024); - mSettings.setPreviewMaxFrameRate(30); - mSettings.setPreviewKeyFrameInterval(1); - mSettings.setPreviewQuality(80); mSettings.setPreviewAutoFocusMode(options.getAutoFocusMode()); mSettings.setPreviewWhiteBalance(options.getAutoWhiteBalanceMode()); mSettings.setPreviewWhiteBalanceTemperature(5600); + if (supportedFpsList.size() > 0) { + mSettings.setPreviewFps(supportedFpsList.get(supportedFpsList.size() - 1)); + } + mSettings.setOrientation(Surface.ROTATION_90); + // 音声設定 mSettings.setPreviewAudioSource(null); - mSettings.setPreviewAudioBitRate(64 * 1024); - mSettings.setPreviewSampleRate(16000); + mSettings.setPreviewAudioBitRate(128 * 1024); + mSettings.setPreviewSampleRate(48000); mSettings.setPreviewChannel(1); mSettings.setUseAEC(true); - mSettings.setMjpegPort(11000 + mFacing.mValue); - mSettings.setMjpegSSLPort(11100 + mFacing.mValue); - mSettings.setRtspPort(12000 + mFacing.mValue); - mSettings.setSrtPort(13000 + mFacing.mValue); + // 各サーバ設定 + mSettings.addEncoder(getId() + "-MJPEG"); + EncoderSettings mjpeg = mSettings.getEncoderSetting(getId() + "-MJPEG"); + mjpeg.setName("MJPEG"); + mjpeg.setMimeType(MimeType.MJPEG); + mjpeg.setPort(11000 + mFacing.mValue); + mjpeg.setPreviewSize(options.getDefaultPreviewSize()); + mjpeg.setPreviewQuality(80); + mjpeg.setPreviewMaxFrameRate(30); + + mSettings.addEncoder(getId() + "-RTSP"); + EncoderSettings rtsp = mSettings.getEncoderSetting(getId() + "-RTSP"); + rtsp.setName("RTSP"); + rtsp.setMimeType(MimeType.RTSP); + rtsp.setPort(12000 + mFacing.mValue); + rtsp.setPreviewSize(options.getDefaultPreviewSize()); + rtsp.setPreviewBitRate(2 * 1024 * 1024); + rtsp.setPreviewMaxFrameRate(30); + rtsp.setPreviewKeyFrameInterval(5); + + mSettings.addEncoder(getId() + "-SRT"); + EncoderSettings srt = mSettings.getEncoderSetting(getId() + "-SRT"); + srt.setName("SRT"); + srt.setMimeType(MimeType.SRT); + srt.setPort(13000 + mFacing.mValue); + srt.setPreviewSize(options.getDefaultPreviewSize()); + srt.setPreviewBitRate(2 * 1024 * 1024); + srt.setPreviewMaxFrameRate(30); + srt.setPreviewKeyFrameInterval(5); + + mSettings.addEncoder(getId() + "-RTMP"); + EncoderSettings rtmp = mSettings.getEncoderSetting(getId() + "-RTMP"); + rtmp.setName("RTMP"); + rtmp.setMimeType(MimeType.RTMP); + rtmp.setPreviewSize(options.getDefaultPreviewSize()); + rtmp.setPreviewBitRate(2 * 1024 * 1024); + rtmp.setPreviewMaxFrameRate(30); + rtmp.setPreviewKeyFrameInterval(5); + rtmp.setBroadcastURI("rtmp://localhost:1935"); mSettings.finishInitialization(); } @@ -207,8 +278,8 @@ public CameraWrapper getCameraWrapper() { @Override public synchronized void clean() { super.clean(); - mCamera2BroadcasterProvider.stopBroadcaster(); - mCamera2PreviewServerProvider.stopServers(); + mCamera2BroadcasterProvider.stop(); + mCamera2PreviewServerProvider.stop(); mCameraSurfaceDrawingThread.stop(true); } @@ -220,7 +291,7 @@ public void destroy() { @Override public String getId() { - return ID_BASE + "_" + mCameraWrapper.getId(); + return ID_BASE + "_" + mCameraWrapper.getId().replaceAll("/", "_"); } @Override @@ -241,6 +312,7 @@ public String getMimeType() { @Override public List getSupportedMimeTypes() { List mimeTypes = mCamera2PreviewServerProvider.getSupportedMimeType(); + mimeTypes.addAll(mCamera2BroadcasterProvider.getSupportedMimeType()); mimeTypes.add(0, MIME_TYPE_JPEG); return mimeTypes; } @@ -409,8 +481,13 @@ private void takePhotoInternal(final @NonNull OnPhotoEventListener listener) { return; } + int currentRotation = mCurrentRotation; + if (getSettings().getOrientation() != -1) { + currentRotation = getSettings().getOrientation(); + } + byte[] jpeg = ImageUtil.convertToJPEG(photo); - int deviceRotation = ROTATIONS.get(mCurrentRotation); + int deviceRotation = ROTATIONS.get(currentRotation); int cameraRotation = mCameraWrapper.getSensorOrientation(); int degrees = (360 - deviceRotation + cameraRotation) % 360; if (mFacing == CameraFacing.FRONT) { @@ -493,14 +570,24 @@ public static CameraFacing detect(CameraWrapper cameraWrapper) { } private class CameraSettings extends Settings { - private List mSupportedPictureSize = new ArrayList<>(); private List mSupportedPreviewSize = new ArrayList<>(); + private List mSupportedEncoderSize; CameraSettings(Context context, HostMediaRecorder recorder) { super(context, recorder); } + @Override + protected EncoderSettings createEncoderSettings(String encoderId) { + return new EncoderSettings(getContext(), encoderId) { + @Override + public List getSupportedEncoderSizes() { + return mSupportedEncoderSize; + } + }; + } + @Override public List getSupportedPictureSizes() { return mSupportedPictureSize; diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2SRTBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2SRTBroadcaster.java index 54e3ea26c2..e71ddbd2c1 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2SRTBroadcaster.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2SRTBroadcaster.java @@ -1,20 +1,18 @@ package org.deviceconnect.android.deviceplugin.host.recorder.camera; import org.deviceconnect.android.deviceplugin.host.recorder.AbstractSRTBroadcaster; -import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; public class Camera2SRTBroadcaster extends AbstractSRTBroadcaster { - public Camera2SRTBroadcaster(Camera2Recorder recorder, String broadcastURI) { - super(recorder, broadcastURI); + public Camera2SRTBroadcaster(Camera2Recorder recorder, String id) { + super(recorder, id); } @Override protected VideoEncoder createVideoEncoder() { Camera2Recorder recorder = (Camera2Recorder) getRecorder(); - HostMediaRecorder.Settings settings = recorder.getSettings(); - switch (settings.getPreviewEncoderName()) { + switch (getEncoderSettings().getPreviewEncoderName()) { case H264: default: return new CameraVideoEncoder(recorder); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2SRTPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2SRTPreviewServer.java index 126d6f4037..a7d97a5f40 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2SRTPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/Camera2SRTPreviewServer.java @@ -1,22 +1,17 @@ package org.deviceconnect.android.deviceplugin.host.recorder.camera; -import android.content.Context; - import org.deviceconnect.android.deviceplugin.host.recorder.AbstractSRTPreviewServer; -import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; public class Camera2SRTPreviewServer extends AbstractSRTPreviewServer { - Camera2SRTPreviewServer(final Context context, final Camera2Recorder recorder, final int port) { - super(context, recorder); - setPort(port); + Camera2SRTPreviewServer(Camera2Recorder recorder, String encoderId) { + super(recorder, encoderId); } @Override protected VideoEncoder createVideoEncoder() { Camera2Recorder recorder = (Camera2Recorder) getRecorder(); - HostMediaRecorder.Settings settings = recorder.getSettings(); - switch (settings.getPreviewEncoderName()) { + switch (getEncoderSettings().getPreviewEncoderName()) { case H264: default: return new CameraVideoEncoder(recorder); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraMJPEGEncoder.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraMJPEGEncoder.java index 37678b9441..7a7adf0059 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraMJPEGEncoder.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraMJPEGEncoder.java @@ -3,26 +3,7 @@ import org.deviceconnect.android.libmedia.streaming.mjpeg.SurfaceMJPEGEncoder; public class CameraMJPEGEncoder extends SurfaceMJPEGEncoder { - CameraMJPEGEncoder(Camera2Recorder 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/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraSurfaceDrawingThread.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraSurfaceDrawingThread.java index 6c3e88350f..92d52d0122 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraSurfaceDrawingThread.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraSurfaceDrawingThread.java @@ -7,6 +7,7 @@ import android.view.WindowManager; import org.deviceconnect.android.deviceplugin.host.camera.CameraWrapper; +import org.deviceconnect.android.deviceplugin.host.camera.CameraWrapperException; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; import org.deviceconnect.android.libmedia.streaming.gles.EGLSurfaceDrawingThread; import org.deviceconnect.android.libmedia.streaming.gles.SurfaceTextureManager; @@ -28,11 +29,15 @@ public CameraSurfaceDrawingThread(Camera2Recorder recorder) { @Override public int getDisplayRotation() { - WindowManager wm = (WindowManager) mRecorder.getContext().getSystemService(Context.WINDOW_SERVICE); - if (wm == null) { - throw new RuntimeException("WindowManager is not supported."); + int orientation = mRecorder.getSettings().getOrientation(); + if (orientation == -1) { + WindowManager wm = (WindowManager) mRecorder.getContext().getSystemService(Context.WINDOW_SERVICE); + if (wm == null) { + throw new RuntimeException("WindowManager is not supported."); + } + return wm.getDefaultDisplay().getRotation(); } - return wm.getDefaultDisplay().getRotation(); + return orientation; } @Override @@ -59,69 +64,64 @@ public boolean isSwappedDimensions() { @Override protected void onStarted() { - startCamera(getSurfaceTexture()); + try { + startCamera(getSurfaceTexture()); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override protected void onStopped() { - stopCamera(); + try { + stopCamera(); + } catch (Exception e) { + // ignore. + } } @Override public void start() { HostMediaRecorder.Settings settings = mRecorder.getSettings(); Size previewSize = settings.getPreviewSize(); - if (previewSize != null) { - setSize(previewSize.getWidth(), previewSize.getHeight()); - setDrawingRange(settings.getDrawingRange()); - super.start(); - } + int w = isSwappedDimensions() ? previewSize.getHeight() : previewSize.getWidth(); + int h = isSwappedDimensions() ? previewSize.getWidth() : previewSize.getHeight(); + setSize(w, h); + super.start(); } @Override protected SurfaceTextureManager createStManager() { + HostMediaRecorder.Settings settings = mRecorder.getSettings(); + Size previewSize = settings.getPreviewSize(); SurfaceTextureManager manager = new SurfaceTextureManager(); SurfaceTexture st = manager.getSurfaceTexture(); - st.setDefaultBufferSize(getWidth(), getHeight()); - if (getDrawingRange() != null) { - // カメラは描画時に端末の向きによって回転するので、ここでは描画範囲の計算も回転してから行う - int w = isSwappedDimensions() ? getHeight() : getWidth(); - int h = isSwappedDimensions() ? getWidth() : getHeight(); - manager.setDrawingRange(getDrawingRange(), w, h); - } + st.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); return manager; } - private void startCamera(SurfaceTexture surfaceTexture) { - try { - HostMediaRecorder.Settings settings = mRecorder.getSettings(); - CameraWrapper cameraWrapper = mRecorder.getCameraWrapper(); - cameraWrapper.getOptions().setPictureSize(settings.getPictureSize()); - cameraWrapper.getOptions().setPreviewSize(settings.getPreviewSize()); - cameraWrapper.getOptions().setFps(settings.getPreviewMaxFrameRate()); - cameraWrapper.getOptions().setAutoFocusMode(settings.getPreviewAutoFocusMode()); - cameraWrapper.getOptions().setAutoWhiteBalanceMode(settings.getPreviewWhiteBalance()); - cameraWrapper.getOptions().setWhiteBalanceTemperature(settings.getPreviewWhiteBalanceTemperature()); - cameraWrapper.getOptions().setAutoExposureMode(settings.getAutoExposureMode()); - cameraWrapper.getOptions().setSensorExposureTime(settings.getSensorExposureTime()); - cameraWrapper.getOptions().setSensorSensitivity(settings.getSensorSensitivity()); - cameraWrapper.getOptions().setSensorFrameDuration(settings.getSensorFrameDuration()); - cameraWrapper.getOptions().setStabilizationMode(settings.getStabilizationMode()); - cameraWrapper.getOptions().setOpticalStabilizationMode(settings.getOpticalStabilizationMode()); - cameraWrapper.getOptions().setDigitalZoom(settings.getDigitalZoom()); - cameraWrapper.getOptions().setNoiseReductionMode(settings.getNoiseReduction()); - cameraWrapper.getOptions().setFocalLength(settings.getFocalLength()); - cameraWrapper.startPreview(new Surface(surfaceTexture)); - } catch (Exception e) { - throw new RuntimeException(e); - } + private void startCamera(SurfaceTexture surfaceTexture) throws CameraWrapperException { + HostMediaRecorder.Settings settings = mRecorder.getSettings(); + CameraWrapper cameraWrapper = mRecorder.getCameraWrapper(); + cameraWrapper.getOptions().setPictureSize(settings.getPictureSize()); + cameraWrapper.getOptions().setPreviewSize(settings.getPreviewSize()); + cameraWrapper.getOptions().setFps(settings.getPreviewFps()); + cameraWrapper.getOptions().setAutoFocusMode(settings.getPreviewAutoFocusMode()); + cameraWrapper.getOptions().setAutoWhiteBalanceMode(settings.getPreviewWhiteBalance()); + cameraWrapper.getOptions().setWhiteBalanceTemperature(settings.getPreviewWhiteBalanceTemperature()); + cameraWrapper.getOptions().setAutoExposureMode(settings.getAutoExposureMode()); + cameraWrapper.getOptions().setSensorExposureTime(settings.getSensorExposureTime()); + cameraWrapper.getOptions().setSensorSensitivity(settings.getSensorSensitivity()); + cameraWrapper.getOptions().setSensorFrameDuration(settings.getSensorFrameDuration()); + cameraWrapper.getOptions().setStabilizationMode(settings.getStabilizationMode()); + cameraWrapper.getOptions().setOpticalStabilizationMode(settings.getOpticalStabilizationMode()); + cameraWrapper.getOptions().setDigitalZoom(settings.getDigitalZoom()); + cameraWrapper.getOptions().setNoiseReductionMode(settings.getNoiseReduction()); + cameraWrapper.getOptions().setFocalLength(settings.getFocalLength()); + cameraWrapper.startPreview(new Surface(surfaceTexture)); } private void stopCamera() { - try { - mRecorder.getCameraWrapper().stopPreview(); - } catch (Exception e) { - // ignore. - } + mRecorder.getCameraWrapper().stopPreview(); } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraVideoEncoder.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraVideoEncoder.java index c39c186efd..cf4ca573d0 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraVideoEncoder.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/camera/CameraVideoEncoder.java @@ -28,14 +28,4 @@ public CameraVideoEncoder(Camera2Recorder recorder, String mimeType) { public VideoQuality getVideoQuality() { return mVideoQuality; } - - // SurfaceVideoEncoder - - @Override - protected void onStartSurfaceDrawing() { - } - - @Override - protected void onStopSurfaceDrawing() { - } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastBroadcasterProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastBroadcasterProvider.java index 140084d553..fbb047d1dc 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastBroadcasterProvider.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastBroadcasterProvider.java @@ -3,25 +3,20 @@ import android.content.Context; import org.deviceconnect.android.deviceplugin.host.recorder.AbstractBroadcastProvider; -import org.deviceconnect.android.deviceplugin.host.recorder.Broadcaster; +import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; +import org.deviceconnect.android.deviceplugin.host.recorder.LiveStreaming; public class ScreenCastBroadcasterProvider extends AbstractBroadcastProvider { - private final ScreenCastRecorder mRecorder; - public ScreenCastBroadcasterProvider(Context context, ScreenCastRecorder recorder) { super(context, recorder); - mRecorder = recorder; } @Override - public Broadcaster createBroadcaster(String broadcastURI) { - if (broadcastURI.startsWith("srt://")) { - return new ScreenCastSRTBroadcaster(mRecorder, broadcastURI); - } else if (broadcastURI.startsWith("rtmp://") || broadcastURI.startsWith("rtmps://")) { - return new ScreenCastRTMPBroadcaster(mRecorder, broadcastURI); - } else { - return null; + public LiveStreaming createLiveStreaming(String encoderId, HostMediaRecorder.EncoderSettings encoderSettings) { + if (encoderSettings.getMimeType() == HostMediaRecorder.MimeType.RTMP) { + return new ScreenCastRTMPBroadcaster((ScreenCastRecorder) getRecorder(), encoderId); } + return null; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastMJPEGEncoder.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastMJPEGEncoder.java index 964a39c031..8c3e2c59b8 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastMJPEGEncoder.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastMJPEGEncoder.java @@ -3,24 +3,7 @@ import org.deviceconnect.android.libmedia.streaming.mjpeg.SurfaceMJPEGEncoder; public class ScreenCastMJPEGEncoder extends SurfaceMJPEGEncoder { - ScreenCastMJPEGEncoder(ScreenCastRecorder recorder) { super(recorder.getSurfaceDrawingThread()); } - - @Override - protected void prepare() { - } - - @Override - protected void startRecording() { - } - - @Override - protected void stopRecording() { - } - - @Override - protected void release() { - } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastMJPEGPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastMJPEGPreviewServer.java index 049af34fb6..547bda2aeb 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastMJPEGPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastMJPEGPreviewServer.java @@ -9,10 +9,8 @@ @TargetApi(Build.VERSION_CODES.LOLLIPOP) class ScreenCastMJPEGPreviewServer extends AbstractMJPEGPreviewServer { - - ScreenCastMJPEGPreviewServer(Context context, ScreenCastRecorder recorder, int port, boolean useSSL) { - super(context, recorder, useSSL); - setPort(port); + ScreenCastMJPEGPreviewServer(ScreenCastRecorder recorder, String encoderId) { + super(recorder, encoderId); } @Override diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastPreviewServerProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastPreviewServerProvider.java index 30f1143ef4..5bf23c4e11 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastPreviewServerProvider.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastPreviewServerProvider.java @@ -4,6 +4,9 @@ import org.deviceconnect.android.deviceplugin.host.recorder.AbstractPreviewServerProvider; import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; +import org.deviceconnect.android.deviceplugin.host.recorder.LiveStreaming; + +import java.util.List; /** * スクリーンキャストのプレビューを配信するサーバを管理するクラス. @@ -11,12 +14,18 @@ class ScreenCastPreviewServerProvider extends AbstractPreviewServerProvider { ScreenCastPreviewServerProvider(Context context, ScreenCastRecorder recorder) { super(context, recorder); + } - HostMediaRecorder.Settings settings = recorder.getSettings(); - - addServer(new ScreenCastMJPEGPreviewServer(context, recorder, settings.getMjpegPort(), false)); - addServer(new ScreenCastMJPEGPreviewServer(context, recorder, settings.getMjpegSSLPort(), true)); - addServer(new ScreenCastRTSPPreviewServer(context, recorder, settings.getRtspPort())); - addServer(new ScreenCastSRTPreviewServer(context, recorder, settings.getSrtPort())); + @Override + public LiveStreaming createLiveStreaming(String encoderId, HostMediaRecorder.EncoderSettings encoderSettings) { + switch (encoderSettings.getMimeType()) { + case MJPEG: + return new ScreenCastMJPEGPreviewServer((ScreenCastRecorder) getRecorder(), encoderId); + case RTSP: + return new ScreenCastRTSPPreviewServer((ScreenCastRecorder) getRecorder(), encoderId); + case SRT: + return new ScreenCastSRTPreviewServer((ScreenCastRecorder) getRecorder(), encoderId); + } + return null; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRTMPBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRTMPBroadcaster.java index 3a7a3b3f27..1386dd0194 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRTMPBroadcaster.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRTMPBroadcaster.java @@ -4,8 +4,8 @@ import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; public class ScreenCastRTMPBroadcaster extends AbstractRTMPBroadcaster { - public ScreenCastRTMPBroadcaster(ScreenCastRecorder recorder, String broadcastURI) { - super(recorder, broadcastURI); + public ScreenCastRTMPBroadcaster(ScreenCastRecorder recorder, String encoderId) { + super(recorder, encoderId); } @Override diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRTSPPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRTSPPreviewServer.java index 0645c27c4e..3c9e452853 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRTSPPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRTSPPreviewServer.java @@ -1,31 +1,27 @@ package org.deviceconnect.android.deviceplugin.host.recorder.screen; -import android.content.Context; import android.os.Build; import androidx.annotation.RequiresApi; import org.deviceconnect.android.deviceplugin.host.recorder.AbstractRTSPPreviewServer; -import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; import org.deviceconnect.android.libmedia.streaming.rtsp.session.video.VideoStream; @RequiresApi(Build.VERSION_CODES.LOLLIPOP) class ScreenCastRTSPPreviewServer extends AbstractRTSPPreviewServer { - ScreenCastRTSPPreviewServer(Context context, ScreenCastRecorder recorder, int port) { - super(context, recorder); - setPort(port); + ScreenCastRTSPPreviewServer(ScreenCastRecorder recorder, String encoderId) { + super(recorder, encoderId); } @Override protected VideoStream createVideoStream() { ScreenCastRecorder recorder = (ScreenCastRecorder) getRecorder(); - HostMediaRecorder.Settings settings = recorder.getSettings(); - switch (settings.getPreviewEncoderName()) { + switch (getEncoderSettings().getPreviewEncoderName()) { case H264: default: - return new ScreenCastH264VideoStream(recorder, 5006); + return new ScreenCastH264VideoStream(recorder, 5016); case H265: - return new ScreenCastH265VideoStream(recorder, 5006); + return new ScreenCastH265VideoStream(recorder, 5016); } } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRecorder.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRecorder.java index 5230447ff2..d11bb0a370 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRecorder.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastRecorder.java @@ -90,15 +90,22 @@ public ScreenCastRecorder(Context context, FileManager fileMgr, MediaProjectionP mScreenCastBroadcasterProvider = new ScreenCastBroadcasterProvider(context, this); } + private List getEncoderSizeList(List supportPreviewSizes) { + List sizes = new ArrayList<>(); + Size maxSize = CapabilityUtil.getSupportedMaxSize(VideoCodec.H264.getMimeType()); + for (Size size : supportPreviewSizes) { + if (size.getWidth() <= maxSize.getWidth() && size.getHeight() <= maxSize.getHeight() + || size.getWidth() <= maxSize.getHeight() && size.getHeight() <= maxSize.getWidth()) { + sizes.add(size); + } + } + return sizes; + } + /** * レコーダの設定を初期化します. */ private void initSupportedSettings() { - // MediaCodec でエンコードできる最大解像度を取得 - // TODO h264, h265 で最大解像度が違う場合はどうするべきか? - // TODO ハードウェアエンコーダとソフトウェアエンコーダで最大解像度が違うのはどうするべきか? - Size maxSize = CapabilityUtil.getSupportedMaxSize("video/avc"); - Size originalSize = getDisplaySize(); List supportPictureSizes = new ArrayList<>(); List supportPreviewSizes = new ArrayList<>(); @@ -116,14 +123,11 @@ private void initSupportedSettings() { Size size = new Size(width, height); supportPictureSizes.add(size); - if (maxSize != null) { - if (size.getWidth() <= maxSize.getWidth() && size.getHeight() <= maxSize.getHeight()) { - supportPreviewSizes.add(size); - } - } + supportPreviewSizes.add(size); } mSettings.mSupportedPreviewSize = supportPreviewSizes; mSettings.mSupportedPictureSize = supportPictureSizes; + mSettings.mSupportEncoderSizeList = getEncoderSizeList(supportPreviewSizes); List> supportFps = new ArrayList<>(); supportFps.add(new Range<>(30, 30)); @@ -132,20 +136,55 @@ private void initSupportedSettings() { if (!mSettings.isInitialized()) { mSettings.setPreviewSize(mSettings.getSupportedPreviewSizes().get(0)); mSettings.setPictureSize(mSettings.getSupportedPictureSizes().get(0)); - mSettings.setPreviewBitRate(2 * 1024 * 1024); - mSettings.setPreviewMaxFrameRate(30); - mSettings.setPreviewKeyFrameInterval(1); - mSettings.setPreviewQuality(80); + mSettings.setOrientation(Surface.ROTATION_90); - mSettings.setPreviewAudioBitRate(64 * 1024); - mSettings.setPreviewSampleRate(16000); + // 音声設定 + mSettings.setPreviewAudioSource(null); + mSettings.setPreviewAudioBitRate(128 * 1024); + mSettings.setPreviewSampleRate(48000); mSettings.setPreviewChannel(1); mSettings.setUseAEC(true); - mSettings.setMjpegPort(21000); - mSettings.setMjpegSSLPort(21100); - mSettings.setRtspPort(22000); - mSettings.setSrtPort(23000); + // プレビュー配信サーバ設定 + mSettings.addEncoder(getId() + "-MJPEG"); + EncoderSettings mjpeg = mSettings.getEncoderSetting(getId() + "-MJPEG"); + mjpeg.setName("MJPEG"); + mjpeg.setMimeType(MimeType.MJPEG); + mjpeg.setPort(21000); + mjpeg.setPreviewSize(mSettings.getSupportedPreviewSizes().get(0)); + mjpeg.setPreviewQuality(80); + mjpeg.setPreviewMaxFrameRate(30); + + mSettings.addEncoder(getId() + "-RTSP"); + EncoderSettings rtsp = mSettings.getEncoderSetting(getId() + "-RTSP"); + rtsp.setName("RTSP"); + rtsp.setMimeType(MimeType.RTSP); + rtsp.setPort(22000); + rtsp.setPreviewSize(mSettings.getSupportedPreviewSizes().get(0)); + rtsp.setPreviewBitRate(2 * 1024 * 1024); + rtsp.setPreviewMaxFrameRate(30); + rtsp.setPreviewKeyFrameInterval(5); + + mSettings.addEncoder(getId() + "-SRT"); + EncoderSettings srt = mSettings.getEncoderSetting(getId() + "-SRT"); + srt.setName("SRT"); + srt.setMimeType(MimeType.SRT); + srt.setPort(23000); + srt.setPreviewSize(mSettings.getSupportedPreviewSizes().get(0)); + srt.setPreviewBitRate(2 * 1024 * 1024); + srt.setPreviewMaxFrameRate(30); + srt.setPreviewKeyFrameInterval(5); + + // 配信設定 + mSettings.addEncoder(getId() + "-RTMP"); + EncoderSettings rtmp = mSettings.getEncoderSetting(getId() + "-RTMP"); + rtmp.setName("RTMP"); + rtmp.setMimeType(MimeType.RTMP); + rtmp.setPreviewSize(mSettings.getSupportedPreviewSizes().get(0)); + rtmp.setPreviewBitRate(2 * 1024 * 1024); + rtmp.setPreviewMaxFrameRate(30); + rtmp.setPreviewKeyFrameInterval(5); + rtmp.setBroadcastURI("rtmp://localhost:1935"); mSettings.finishInitialization(); } @@ -161,52 +200,36 @@ private Size getDisplaySize() { if (wm == null) { throw new RuntimeException("WindowManager is not supported."); } - DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); - boolean isSwap; - switch (wm.getDefaultDisplay().getRotation()) { - case Surface.ROTATION_0: - case Surface.ROTATION_180: - isSwap = false; - break; - default: - case Surface.ROTATION_90: - case Surface.ROTATION_270: - isSwap = true; - break; - } + int insetsWidth = 0; int insetsHeight = 0; Size displaySize; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); - final WindowInsets windowInsets = windowMetrics.getWindowInsets(); + WindowInsets windowInsets = windowMetrics.getWindowInsets(); Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout()); - - int width = isSwap ? windowMetrics.getBounds().height() :windowMetrics.getBounds().width(); - int height = isSwap ? windowMetrics.getBounds().width() :windowMetrics.getBounds().height(); + int width = windowMetrics.getBounds().width(); + int height = windowMetrics.getBounds().height(); insetsWidth = insets.right + insets.left; insetsHeight = insets.top + insets.bottom; - - // Legacy size that Display#getSize reports - displaySize = new Size(width - insetsWidth, - height - insetsHeight); + displaySize = new Size(width - insetsWidth, height - insetsHeight); } else { - // 画面が回転している場合には、縦横をスワップしておく。 - int width = isSwap ? metrics.heightPixels : metrics.widthPixels; - int height = isSwap ? metrics.widthPixels : metrics.heightPixels; - displaySize = new Size(width, height); + DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); + displaySize = new Size(metrics.widthPixels, metrics.heightPixels); } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Display.Mode[] modes = wm.getDefaultDisplay().getSupportedModes(); - if(modes.length > 0){ + if (modes.length > 0){ Display.Mode mode = modes[modes.length - 1]; - int width = isSwap ? mode.getPhysicalHeight() : mode.getPhysicalWidth(); - int height = isSwap ? mode.getPhysicalWidth() : mode.getPhysicalHeight(); - // 4Kサイズの解像度がある場合はそちらを優先する + int width = mode.getPhysicalWidth(); + int height = mode.getPhysicalHeight(); + // 4K サイズの解像度がある場合はそちらを優先する displaySize = new Size(width - insetsWidth, height - insetsHeight); } } + return displaySize; } @@ -218,8 +241,8 @@ public EGLSurfaceDrawingThread getSurfaceDrawingThread() { @Override public void clean() { super.clean(); - mScreenCastBroadcasterProvider.stopBroadcaster(); - mScreenCastPreviewServerProvider.stopServers(); + mScreenCastBroadcasterProvider.stop(); + mScreenCastPreviewServerProvider.stop(); mScreenCastMgr.clean(); } @@ -242,6 +265,7 @@ public String getMimeType() { @Override public List getSupportedMimeTypes() { List mimeTypes = mScreenCastPreviewServerProvider.getSupportedMimeType(); + mimeTypes.addAll(mScreenCastBroadcasterProvider.getSupportedMimeType()); mimeTypes.add(0, MIME_TYPE_JPEG); return mimeTypes; } @@ -379,15 +403,26 @@ private void takePhotoInternal(final @NonNull OnPhotoEventListener listener) { } } - private static class ScreenCastSettings extends Settings { + private class ScreenCastSettings extends Settings { private List mSupportedPictureSize = new ArrayList<>(); private List mSupportedPreviewSize = new ArrayList<>(); private List> mSupportedFps = new ArrayList<>(); + private List mSupportEncoderSizeList = new ArrayList<>(); ScreenCastSettings(Context context, HostMediaRecorder recorder) { super(context, recorder); } + @Override + protected EncoderSettings createEncoderSettings(String encoderId) { + return new EncoderSettings(getContext(), encoderId) { + @Override + public List getSupportedEncoderSizes() { + return mSupportEncoderSizeList; + } + }; + } + @Override public List getSupportedPictureSizes() { return mSupportedPictureSize; diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSRTBroadcaster.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSRTBroadcaster.java index b7a72b20fa..fdf5e9de1c 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSRTBroadcaster.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSRTBroadcaster.java @@ -1,20 +1,18 @@ package org.deviceconnect.android.deviceplugin.host.recorder.screen; import org.deviceconnect.android.deviceplugin.host.recorder.AbstractSRTBroadcaster; -import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; public class ScreenCastSRTBroadcaster extends AbstractSRTBroadcaster { - public ScreenCastSRTBroadcaster(ScreenCastRecorder recorder, String broadcastURI) { - super(recorder, broadcastURI); + public ScreenCastSRTBroadcaster(ScreenCastRecorder recorder, String name) { + super(recorder, name); } @Override protected VideoEncoder createVideoEncoder() { ScreenCastRecorder recorder = (ScreenCastRecorder) getRecorder(); - HostMediaRecorder.Settings settings = recorder.getSettings(); - switch (settings.getPreviewEncoderName()) { + switch (getEncoderSettings().getPreviewEncoderName()) { case H264: default: return new ScreenCastVideoEncoder(recorder); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSRTPreviewServer.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSRTPreviewServer.java index e52cf97800..29400103e7 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSRTPreviewServer.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSRTPreviewServer.java @@ -6,10 +6,7 @@ */ package org.deviceconnect.android.deviceplugin.host.recorder.screen; -import android.content.Context; - import org.deviceconnect.android.deviceplugin.host.recorder.AbstractSRTPreviewServer; -import org.deviceconnect.android.deviceplugin.host.recorder.HostMediaRecorder; import org.deviceconnect.android.libmedia.streaming.video.VideoEncoder; /** @@ -18,16 +15,14 @@ * @author NTT DOCOMO, INC. */ class ScreenCastSRTPreviewServer extends AbstractSRTPreviewServer { - ScreenCastSRTPreviewServer(final Context context, final ScreenCastRecorder recorder, final int port) { - super(context, recorder); - setPort(port); + ScreenCastSRTPreviewServer(ScreenCastRecorder recorder, String encoderId) { + super(recorder, encoderId); } @Override protected VideoEncoder createVideoEncoder() { ScreenCastRecorder recorder = (ScreenCastRecorder) getRecorder(); - HostMediaRecorder.Settings settings = recorder.getSettings(); - switch (settings.getPreviewEncoderName()) { + switch (getEncoderSettings().getPreviewEncoderName()) { case H264: default: return new ScreenCastVideoEncoder(recorder); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSurfaceDrawingThread.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSurfaceDrawingThread.java index f47da6287e..1be49f8399 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSurfaceDrawingThread.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastSurfaceDrawingThread.java @@ -31,7 +31,7 @@ public ScreenCastSurfaceDrawingThread(ScreenCastRecorder recorder) { // 画面の更新が発生しない場合は、MediaCodec に更新イベントが発生しないので // ここでは、画面更新のタイムアウトを 0 にして、タイムアウトが発生しないように設定 - setTimeout(0); + setRenderingTimeout(0); } // EGLSurfaceDrawingThread @@ -44,7 +44,20 @@ public int getDisplayRotation() { @Override public boolean isSwappedDimensions() { - return mRecorder.getScreenCastMgr().isSwappedDimensions(); + int orientation = mRecorder.getSettings().getOrientation(); + if (orientation == -1) { + return mRecorder.getScreenCastMgr().isSwappedDimensions(); + } + + switch (orientation) { + case Surface.ROTATION_0: + case Surface.ROTATION_180: + return false; + case Surface.ROTATION_90: + case Surface.ROTATION_270: + default: + return true; + } } @Override @@ -61,13 +74,10 @@ protected void onStopped() { public void start() { HostMediaRecorder.Settings settings = mRecorder.getSettings(); Size previewSize = settings.getPreviewSize(); - if (previewSize != null) { - int width = isSwappedDimensions() ? previewSize.getHeight() : previewSize.getWidth(); - int height = isSwappedDimensions() ? previewSize.getWidth() : previewSize.getHeight(); - setSize(width, height); - setDrawingRange(settings.getDrawingRange()); - super.start(); - } + int width = isSwappedDimensions() ? previewSize.getHeight() : previewSize.getWidth(); + int height = isSwappedDimensions() ? previewSize.getWidth() : previewSize.getHeight(); + setSize(width, height); + super.start(); } private void startScreenCast(SurfaceTexture surfaceTexture) { diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastVideoEncoder.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastVideoEncoder.java index a8c6262f41..d5b0e88029 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastVideoEncoder.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/ScreenCastVideoEncoder.java @@ -25,14 +25,4 @@ public ScreenCastVideoEncoder(ScreenCastRecorder recorder, String mimeType) { public VideoQuality getVideoQuality() { return mVideoQuality; } - - // SurfaceVideoEncoder - - @Override - protected void onStartSurfaceDrawing() { - } - - @Override - protected void onStopSurfaceDrawing() { - } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/SurfaceScreenCast.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/SurfaceScreenCast.java index 291c43d3dd..8f1c8c0cb2 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/SurfaceScreenCast.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/screen/SurfaceScreenCast.java @@ -9,7 +9,7 @@ @TargetApi(Build.VERSION_CODES.LOLLIPOP) class SurfaceScreenCast extends AbstractScreenCast { - private Surface mOutputSurface; + private final Surface mOutputSurface; SurfaceScreenCast(Context context, MediaProjection mediaProjection, Surface outputSurface, int width, int height) { diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/ui/PreviewSurfaceView.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/ui/PreviewSurfaceView.java index 0d454615dc..a0a51c34a2 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/ui/PreviewSurfaceView.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/ui/PreviewSurfaceView.java @@ -1,17 +1,39 @@ package org.deviceconnect.android.deviceplugin.host.recorder.ui; +import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.Rect; import android.util.AttributeSet; +import android.util.Log; import android.util.Size; import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.constraintlayout.widget.ConstraintLayout; import org.deviceconnect.android.deviceplugin.host.R; +import org.deviceconnect.android.deviceplugin.host.recorder.CropInterface; + +import java.util.HashMap; +import java.util.Map; public class PreviewSurfaceView extends FrameLayout { + private int mPreviewWidth; + private int mPreviewHeight; + private int mSurfaceWidth; + private int mSurfaceHeight; + private int mDragStartX; + private int mDragStartY; + private boolean mDragFlag; + private boolean mScaleFlag; + private ScaleGestureDetector mScaleGestureDetector; + private final Map mCropRectMap = new HashMap<>(); public PreviewSurfaceView(Context context) { super(context); @@ -19,29 +41,309 @@ public PreviewSurfaceView(Context context) { public PreviewSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); - initView(context, attrs, 0); + initView(context); } public PreviewSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - initView(context, attrs, defStyleAttr); + initView(context); } - private void initView(Context context, AttributeSet attrs, int defStyle) { + @SuppressLint("ClickableViewAccessibility") + private void initView(Context context) { LayoutInflater.from(context).inflate(R.layout.host_preview_surface_view, this); + + mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.OnScaleGestureListener() { + @Override + public boolean onScale(ScaleGestureDetector detector) { + CropRectHolder holder = getFocusedHolder(); + if (holder == null) { + return false; + } + + Rect cropRect = holder.mCropRect; + Object tag = holder.mTag; + + float scaleFactor = Math.max(0.1f, Math.min(detector.getScaleFactor(), 5.0f)); + float newWidth = cropRect.width() * scaleFactor; + float newHeight = cropRect.height() * scaleFactor; + + if (mPreviewWidth < newWidth) { + newWidth = mPreviewWidth; + } + + if (mPreviewHeight < newHeight) { + newHeight = mPreviewHeight; + } + + int diffW = (int) ((newWidth - cropRect.width()) / 2.0f); + int diffH = (int) ((newHeight - cropRect.height()) / 2.0f); + + int newLeft = cropRect.left - diffW; + int newRight = cropRect.right + diffW; + int newTop = cropRect.top - diffH; + int newBottom = cropRect.bottom + diffH; + + cropRect.set(newLeft, newTop, newRight, newBottom); + + int diffX = 0; + int diffY = 0; + if (newLeft < 0) { + diffX = -cropRect.left; + } + + if (newRight >= mPreviewWidth) { + diffX = mPreviewWidth - cropRect.right; + } + + if (newTop < 0) { + diffY = -cropRect.top; + } + + if (newBottom >= mPreviewHeight) { + diffY = mPreviewHeight - cropRect.bottom; + } + + cropRect.offset(diffX, diffY); + onChangedCropRect(tag, cropRect); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + mScaleFlag = true; + mDragFlag = false; + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mScaleFlag = false; + } + }); + + SurfaceView surfaceView = getSurfaceView(); + surfaceView.setOnTouchListener((view, event) -> { + mScaleGestureDetector.onTouchEvent(event); + + if (mScaleFlag || event.getPointerCount() > 1) { + return true; + } + + final int[] anchorPos = new int[2]; + view.getLocationOnScreen(anchorPos); + + float scale = getScale(); + int x = (int) event.getRawX() - anchorPos[0]; + int y = (int) event.getRawY() - anchorPos[1]; + int orgX = (int) (x / scale); + int orgY = (int) (y / scale); + + switch(event.getAction()) { + case MotionEvent.ACTION_DOWN: + { + for (CropRectHolder holder : mCropRectMap.values()) { + Rect cropRect = holder.mCropRect; + if (cropRect.contains(orgX, orgY)) { + clearFocusCropRect(); + mDragStartX = x; + mDragStartY = y; + mDragFlag = true; + holder.mFocused = true; + break; + } + } + } break; + + case MotionEvent.ACTION_MOVE: + { + CropRectHolder holder = getFocusedHolder(); + if (mDragFlag && holder != null) { + Rect cropRect = holder.mCropRect; + Object tag = holder.mTag; + + int diffX = (int) ((x - mDragStartX) / scale); + int diffY = (int) ((y - mDragStartY) / scale); + + int newLeft = cropRect.left + diffX; + int newRight = cropRect.right + diffX; + int newTop = cropRect.top + diffY; + int newBottom = cropRect.bottom + diffY; + + if (newLeft < 0) { + diffX = -cropRect.left; + } + + if (newRight >= mPreviewWidth) { + diffX = mPreviewWidth - cropRect.right; + } + + if (newTop < 0) { + diffY = -cropRect.top; + } + + if (newBottom >= mPreviewHeight) { + diffY = mPreviewHeight - cropRect.bottom; + } + + cropRect.offset(diffX, diffY); + onChangedCropRect(tag, cropRect); + + mDragStartX = x; + mDragStartY = y; + } + } break; + + case MotionEvent.ACTION_UP: + mDragFlag = false; + break; + } + return true; + }); + } + + public float getScale() { + if (mPreviewHeight == 0) { + return 1.0f; + } else { + return mSurfaceHeight / (float) mPreviewHeight; + } + } + + /** + * プレビューの表示を行う SurfaceView を取得します. + * + * @return SurfaceView + */ + public SurfaceView getSurfaceView() { + View root = findViewById(R.id.preview_root); + if (root != null) { + return root.findViewById(R.id.preview_surface_view); + } + return null; + } + + /** + * 切り抜き範囲の枠を追加します. + * + * 既に同じキーが登録されている場合は、切り抜き範囲を変更します。 + * + * @param key 切り抜き範囲の枠を識別するキー + * @param cropRect 切り抜き範囲の枠 + */ + public void addCropRect(Object key, Rect cropRect) { + if (key == null || cropRect == null) { + return; + } + + post(() -> { + CropRectHolder holder = mCropRectMap.get(key); + if (holder == null) { + holder = new CropRectHolder(); + holder.mTag = key; + holder.mCropRect = cropRect; + holder.mView = inflate(getContext(), R.layout.item_crop_frame, null); + + TextView tv = holder.mView.findViewById(R.id.textview); + if (tv != null && key instanceof CropInterface) { + tv.setText(((CropInterface) key).getName()); + } + + ConstraintLayout constraintLayout = findViewById(R.id.preview_root); + constraintLayout.addView(holder.mView); + + // addView を行った後でないと LayoutParams が null になるので注意 + ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) holder.mView.getLayoutParams(); + layoutParams.leftToLeft = R.id.preview_surface_view; + layoutParams.topToTop = R.id.preview_surface_view; + holder.mView.setLayoutParams(layoutParams); + + mCropRectMap.put(key, holder); + } else { + holder.mCropRect = cropRect; + } + + setCropRectView(holder); + }); + } + + /** + * 切り抜き範囲の枠を削除します. + * + * 対応する切り抜き範囲の枠が存在しない場合は何も処理を行いません。 + * + * @param key 切り抜き範囲の枠を識別するキー + */ + public void removeCropRange(Object key) { + post(() -> { + CropRectHolder holder = mCropRectMap.remove(key); + if (holder != null && holder.mView != null) { + ConstraintLayout constraintLayout = findViewById(R.id.preview_root); + constraintLayout.removeView(holder.mView); + } + }); + } + + /** + * 切り抜き範囲の枠の値が変更された時に呼び出されます. + * + * @param key キー + * @param cropRect 新しい値 + */ + protected void onChangedCropRect(Object key, Rect cropRect) { + CropRectHolder holder = mCropRectMap.get(key); + if (holder != null) { + setCropRectView(holder); + } } + private void setCropRectView(CropRectHolder holder) { + Rect cropRect = holder.mCropRect; + View frameView = holder.mView; + if (frameView != null) { + float scale = getScale(); + int left = (int) (cropRect.left * scale); + int top = (int) (cropRect.top * scale); + ViewGroup.LayoutParams layoutParams = frameView.getLayoutParams(); + layoutParams.width = (int) (cropRect.width() * scale); + layoutParams.height = (int) (cropRect.height() * scale); + MarginLayoutParams mlp = (MarginLayoutParams) layoutParams; + mlp.setMargins(left, top, 0, 0); + frameView.setLayoutParams(layoutParams); + frameView.setVisibility(VISIBLE); + } + } + + private void clearFocusCropRect() { + for (CropRectHolder h : mCropRectMap.values()) { + h.mFocused = false; + } + } + + private CropRectHolder getFocusedHolder() { + for (CropRectHolder h : mCropRectMap.values()) { + if (h.mFocused) { + return h; + } + } + return null; + } + + /** + * アスペクトを保持したまま画面全体にプレビューを表示するように調整します. + * + * @param previewWidth プレビューの横幅 + * @param previewHeight プレビューの縦幅 + */ public void fullSurfaceView(int previewWidth, int previewHeight) { post(() -> { int gcd = calculatedGcd(previewWidth, previewHeight); View root = findViewById(R.id.preview_root); SurfaceView surfaceView = root.findViewById(R.id.preview_surface_view); - int cameraWidth = previewWidth; - int cameraHeight = previewHeight; - int widthRatio = cameraWidth / gcd; - int heightRatio = cameraHeight / gcd; + int widthRatio = previewWidth / gcd; + int heightRatio = previewHeight / gcd; int largeRate = widthRatio; if (widthRatio < heightRatio) { @@ -55,14 +357,17 @@ public void fullSurfaceView(int previewWidth, int previewHeight) { int previewGcd = (int) Math.ceil(largeSize / (double) largeRate); - int width = widthRatio * previewGcd; - int height = heightRatio * previewGcd; + mPreviewWidth = previewWidth; + mPreviewHeight = previewHeight; + mSurfaceWidth = widthRatio * previewGcd; + mSurfaceHeight = heightRatio * previewGcd; ViewGroup.LayoutParams layoutParams = surfaceView.getLayoutParams(); - layoutParams.width = width; - layoutParams.height = height; + layoutParams.width = mSurfaceWidth; + layoutParams.height = mSurfaceHeight; surfaceView.setLayoutParams(layoutParams); - surfaceView.getHolder().setFixedSize(previewWidth, previewHeight); + surfaceView.getHolder().setFixedSize(mSurfaceWidth, mSurfaceHeight); +// surfaceView.getHolder().setFixedSize(previewWidth, previewHeight); }); } @@ -80,14 +385,26 @@ public void adjustSurfaceView(int previewWidth, int previewHeight) { Size viewSize = new Size(root.getWidth(), root.getHeight()); Size changeSize = calculateViewSize(previewWidth, previewHeight, viewSize); + mPreviewWidth = previewWidth; + mPreviewHeight = previewHeight; + mSurfaceWidth = changeSize.getWidth(); + mSurfaceHeight = changeSize.getHeight(); + ViewGroup.LayoutParams layoutParams = surfaceView.getLayoutParams(); - layoutParams.width = changeSize.getWidth(); - layoutParams.height = changeSize.getHeight(); + layoutParams.width = mSurfaceWidth; + layoutParams.height = mSurfaceHeight; surfaceView.setLayoutParams(layoutParams); - surfaceView.getHolder().setFixedSize(previewWidth, previewHeight); + surfaceView.getHolder().setFixedSize(mSurfaceWidth, mSurfaceHeight); +// surfaceView.getHolder().setFixedSize(previewWidth, previewHeight); }); } + /** + * a と b の最大公約数を計算します. + * @param a 値1 + * @param b 値2 + * @return a と b の最大公約数 + */ private int calculatedGcd(int a, int b) { if (b == 0) { return a; @@ -115,4 +432,11 @@ private Size calculateViewSize(int width, int height, Size viewSize) { } return new Size(viewSize.getWidth(), h); } + + private static class CropRectHolder { + private Object mTag; + private Rect mCropRect; + private View mView; + private boolean mFocused; + } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/CapabilityUtil.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/CapabilityUtil.java index 69c680fda5..e63e1bdf44 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/CapabilityUtil.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/CapabilityUtil.java @@ -44,7 +44,10 @@ public static List getSupportedProfileLevel(Stri MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(mimeType); if (codecCapabilities.profileLevels != null) { for (MediaCodecInfo.CodecProfileLevel c : codecCapabilities.profileLevels) { - list.add(new HostMediaRecorder.ProfileLevel(c.profile, c.level)); + HostMediaRecorder.ProfileLevel pl = new HostMediaRecorder.ProfileLevel(c.profile, c.level); + if (!list.contains(pl)) { + list.add(pl); + } } } } @@ -54,6 +57,16 @@ public static List getSupportedProfileLevel(Stri return list; } + public static boolean isSupportedProfileLevel(String mimeType, int profile, int level) { + List list = getSupportedProfileLevel(mimeType); + for (HostMediaRecorder.ProfileLevel pl : list) { + if (pl.getProfile() == profile && pl.getLevel() == level) { + return true; + } + } + return false; + } + private static List getSupportedEncoders(String mimeType) { List encoderList = new ArrayList<>(); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/MediaProjectionProvider.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/MediaProjectionProvider.java index 475bd5cdca..7c9b019c12 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/MediaProjectionProvider.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/MediaProjectionProvider.java @@ -122,7 +122,11 @@ protected void onReceiveResult(final int resultCode, final Bundle resultData) { // 画面に HOST プラグイン関連の画面が表示されている場合は、Activity が起動できるので // そのまま Context#startActivity を実行します。 if (getApp().isDeviceConnectClassOfTopActivity() || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - mContext.startActivity(intent); + try { + mContext.startActivity(intent); + } catch (Exception e) { + callback.onDisallowed(); + } } else { // Android 10(Q) からは、バックグラウンドから Activity を起動できなくなったので、 // Notification から起動するようにします。 diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/MovingRectThread.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/MovingRectThread.java new file mode 100644 index 0000000000..3abdab1ac5 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/MovingRectThread.java @@ -0,0 +1,236 @@ +package org.deviceconnect.android.deviceplugin.host.recorder.util; + +import android.graphics.Rect; + +import org.deviceconnect.android.libmedia.streaming.util.QueueThread; +import org.deviceconnect.android.libmedia.streaming.util.WeakReferenceList; + +public class MovingRectThread { + private final WeakReferenceList mOnEventListeners = new WeakReferenceList<>(); + private WorkThread mWorkThread; + private int mFrameRate = 30; + + /** + * スレッドを動作させるフレームレートを設定します. + * + * @param frameRate フレームレート + */ + public void setFrameRate(int frameRate) { + if (frameRate <= 0) { + throw new IllegalArgumentException("frameRate is negative or zero."); + } + if (frameRate >= 1000) { + throw new IllegalArgumentException("frameRate is over 1000."); + } + mFrameRate = frameRate; + } + + /** + * 範囲指定の移動処理を開始します. + */ + public synchronized void start() { + if (mWorkThread != null) { + return; + } + mWorkThread = new WorkThread(); + mWorkThread.setName("MovingRectThread"); + mWorkThread.start(); + } + + /** + * 範囲指定の移動処理を停止します. + */ + public synchronized void stop() { + if (mWorkThread != null) { + mWorkThread.terminate(); + mWorkThread = null; + } + } + + /** + * 矩形を設定します. + * + * @param rect 矩形 + */ + public void set(Rect rect) { + move(null, rect, 0); + } + + /** + * 描画範囲の移動を設定します. + * + * @param start 開始する矩形 + * @param end 停止する矩形 + * @param duration 移動時間(ミリ秒) + * @param cancel 移動中の処理をキャンセルするか設定 + */ + public synchronized void move(Rect start, Rect end, int duration, boolean cancel) { + if (mWorkThread == null) { + return; + } + + if (duration < 0 || end == null) { + return; + } + + if (cancel) { + mWorkThread.cancel(); + } + + MovingObject object = new MovingObject(); + object.mStartRect = start; + object.mEndRect = end; + object.mDuration = duration; + object.mInterval = 1000 / mFrameRate; + mWorkThread.add(object); + } + + /** + * 描画範囲の移動を設定します. + * + * 追加された時にアニメーション中の場合には、移動をキャンセルして、次の移動を開始します。 + * + * @param start 開始する矩形 + * @param end 停止する矩形 + * @param duration 移動時間(ミリ秒) + */ + public void move(Rect start, Rect end, int duration) { + move(start, end, duration, true); + } + + /** + * 現在動いている範囲を停止します. + * + * 動いている範囲が無い場合は何もしません。 + */ + public void cancelMove() { + if (mWorkThread != null) { + mWorkThread.cancel(); + } + } + + /** + * 範囲の移動イベントを通知するリスナーを設定します. + * + * @param listener 追加するリスナー + */ + public void addOnEventListener(OnEventListener listener) { + if (listener != null) { + mOnEventListeners.add(listener); + } + } + + /** + * 範囲の移動イベントを通知するリスナーを削除します. + * + * @param listener 削除するリスナー + */ + public void removeOnEventListener(OnEventListener listener) { + if (listener != null) { + mOnEventListeners.remove(listener); + } + } + + public interface OnEventListener { + void onMoved(Rect rect); + } + + private void postOnMoved(Rect rect) { + for (OnEventListener cb : mOnEventListeners) { + try { + cb.onMoved(rect); + } catch (Exception e) { + // ignore. + } + } + } + + private class MovingObject implements Runnable { + private boolean mStopFlag = false; + private final Rect mRect = new Rect(); + private Rect mStartRect; + private Rect mEndRect; + private int mDuration; + private int mInterval; + + public void terminate() { + mStopFlag = true; + } + + @Override + public void run() { + int count = mDuration / mInterval; + if (count > 0 && mStartRect != null) { + float c = mDuration / (float) mInterval; + float deltaLeft = (mEndRect.left - mStartRect.left) / c; + float deltaTop = (mEndRect.top - mStartRect.top) / c; + float deltaRight = (mEndRect.right - mStartRect.right) / c; + float deltaBottom = (mEndRect.bottom - mStartRect.bottom) / c; + + float left = mStartRect.left; + float top = mStartRect.top; + float right = mStartRect.right; + float bottom = mStartRect.bottom; + + while (!mStopFlag && count > 0) { + left += deltaLeft; + top += deltaTop; + right += deltaRight; + bottom += deltaBottom; + mRect.set((int) left, (int) top, (int) right, (int) bottom); + count--; + + postOnMoved(mRect); + + try { + Thread.sleep(mInterval); + } catch (Exception e) { + return; + } + } + } + + if (count == 0) { + mRect.set(mEndRect); + postOnMoved(mRect); + } + } + } + + private static class WorkThread extends QueueThread { + private boolean mStopFlag = false; + private MovingObject mMovingObject; + + private void cancel() { + if (mMovingObject != null) { + mMovingObject.terminate(); + } + } + + /** + * スレッドを終了します. + */ + private void terminate() { + mStopFlag = true; + cancel(); + interrupt(); + try { + join(500); + } catch (InterruptedException e) { + // ignore. + } + } + + @Override + public void run() { + try { + while (!mStopFlag) { + mMovingObject = get(); + mMovingObject.run(); + } + } catch (InterruptedException e) { + // ignore. + } + } + } +} diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/OverlayPermissionActivity.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/OverlayPermissionActivity.java index 4fb2fc86f0..3ad4594342 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/OverlayPermissionActivity.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/OverlayPermissionActivity.java @@ -44,6 +44,13 @@ public void onCreate(final Bundle savedInstanceState) { showOverlayDisabled(); } + @Override + public void finish() { + super.finish(); + // Activity の遷移アニメーションを削除 + overridePendingTransition(0, 0); + } + @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/PermissionReceiverActivity.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/PermissionReceiverActivity.java index 8709784160..cf07d2b199 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/PermissionReceiverActivity.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/PermissionReceiverActivity.java @@ -68,6 +68,13 @@ protected void onResume() { startActivityForResult(intent, REQUEST_CODE); } + @Override + public void finish() { + super.finish(); + // Activity の遷移アニメーションを削除 + overridePendingTransition(0, 0); + } + @Override protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/PropertyUtil.java b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/PropertyUtil.java index f65a4bd979..d241287ad2 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/PropertyUtil.java +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/java/org/deviceconnect/android/deviceplugin/host/recorder/util/PropertyUtil.java @@ -5,6 +5,11 @@ import android.graphics.Rect; import android.util.Size; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; import java.util.Set; /** @@ -59,6 +64,38 @@ public void put(String leftKey, String topKey, String rightKey, String bottomKey .apply(); } + public void put(String key, List values) { + StringBuilder value = new StringBuilder(); + for (String v : values) { + if (value.length() > 0) { + value.append(","); + } + try { + value.append(URLEncoder.encode(v, "UTF-8")); + } catch (UnsupportedEncodingException e) { + // ignore. + } + } + mPref.edit().putString(key, value.toString()).apply(); + } + + public List getArrayString(String key) { + String string = mPref.getString(key, null); + if (string == null) { + return new ArrayList<>(); + } + List array = new ArrayList<>(); + String[] split = string.split(","); + for (String v : split) { + try { + array.add(URLDecoder.decode(v, "UTF-8")); + } catch (UnsupportedEncodingException e) { + // ignore. + } + } + return array; + } + public Integer getInteger(String key, Integer defaultValue) { String value = mPref.getString(key, String.valueOf(defaultValue)); try { diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/drawable/border_red.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/drawable/border_red.xml new file mode 100644 index 0000000000..3ca4e29a7a --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/drawable/border_red.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/layout/host_preview_surface_view.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/layout/host_preview_surface_view.xml index 65fab07454..d0a9ea3c08 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/layout/host_preview_surface_view.xml +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/layout/host_preview_surface_view.xml @@ -1,6 +1,7 @@ + + \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/layout/item_crop_frame.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/layout/item_crop_frame.xml new file mode 100644 index 0000000000..2c1939b7e1 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/layout/item_crop_frame.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/navigation/navigation_recorder_settings.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/navigation/navigation_recorder_settings.xml index 8b939a19b3..348aca366e 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/navigation/navigation_recorder_settings.xml +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/navigation/navigation_recorder_settings.xml @@ -24,6 +24,22 @@ app:popEnterAnim="@anim/nav_slide_pop_enter_anim" app:popExitAnim="@anim/nav_slide_pop_exit_anim"/> + + + + + + + + + - @@ -64,14 +88,20 @@ + android:id="@+id/host_settings_mjpeg" + android:name="org.deviceconnect.android.deviceplugin.host.activity.recorder.settings.SettingsMJPEGFragment"> + android:id="@+id/host_settings_rtsp" + android:name="org.deviceconnect.android.deviceplugin.host.activity.recorder.settings.SettingsRTSPFragment"> + + + + diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values-ja/strings.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values-ja/strings.xml index c622b32663..214279677a 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values-ja/strings.xml +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values-ja/strings.xml @@ -79,13 +79,21 @@ 映像設定 音声設定 配信設定 - SRT 設定 + MJPEG + RTSP + SRT + RTMP ポート設定 + サーバ設定 + 接続エラー + カメラが接続されていません。 + はい 静止画解像度 プレビュー解像度 フレームレート (fps) + 向き ビットレート (bps) ビットレートモード キーフレームインターバル(秒) @@ -113,6 +121,8 @@ Right Bottom リセット + 配信解像度を入力する + クリップボードにコピーしました 音声有効 @@ -142,6 +152,7 @@ RTSP 設定 SRT 設定 ポート番号 + URL 自動 diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values/arrays.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values/arrays.xml index 652a70c2e0..fed36f6760 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values/arrays.xml +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values/arrays.xml @@ -5,11 +5,28 @@ フロント バック + 0 1 + + 自由回転 + 0度 + 90度 + 180度 + 270度 + + + + -1 + 0 + 1 + 2 + 3 + + H264 H265 diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values/strings.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values/strings.xml index 8526b2814f..864baf67e5 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values/strings.xml +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/values/strings.xml @@ -78,12 +78,20 @@ Video Settings Audio Settings Broadcast Settings - SRT Settings + MJPEG + RTSP + SRT + RTMP Port Settings + Server Settings + Error + The camera is not connected. + OK Still Image Resolution Preview Resolution Frame rate (fps) + Orientation Bitrate (bps) Bitrate Mode Key frame interval (sec) @@ -111,6 +119,8 @@ Right Bottom Reset + Set by delivery resolution + Copied to the clipboard Enable Set whether to enable preview audio. @@ -136,6 +146,7 @@ RTSP Settings SRT Settings Port + URL SRTO_PEERLATENCY SRTO_LOSSMAXTTL diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_broadcast.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_broadcast.xml index 3534a97906..d34fc24f04 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_broadcast.xml +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_broadcast.xml @@ -29,6 +29,111 @@ app:iconSpaceReserved="false" app:key="broadcast_retry_interval" app:useSimpleSummaryProvider="true" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_main.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_main.xml index ee025e7077..4461dc60bf 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_main.xml +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_main.xml @@ -6,30 +6,32 @@ android:title="@string/host_recorder_settings_title" app:iconSpaceReserved="false"> - - - + - + + + + + - \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_mjpeg.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_mjpeg.xml new file mode 100644 index 0000000000..f332ec1946 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_mjpeg.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_port.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_port.xml deleted file mode 100644 index ae41d60e96..0000000000 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_port.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_rtsp.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_rtsp.xml new file mode 100644 index 0000000000..b0c13eaec4 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_rtsp.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_srt.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_srt.xml index d4c8e7d198..9fc5124a47 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_srt.xml +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_srt.xml @@ -2,57 +2,187 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_video.xml b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_video.xml index 40d901bffd..6a6c24aa55 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_video.xml +++ b/dConnectDevicePlugin/dConnectDeviceHost/app/src/main/res/xml/settings_host_recorder_video.xml @@ -18,54 +18,17 @@ app:iconSpaceReserved="false" app:useSimpleSummaryProvider="true" /> - - - - - - - - - - - - @@ -147,53 +110,4 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/app/build.gradle b/dConnectDevicePlugin/dConnectDeviceUVC/app/build.gradle index 646eae2b8e..2d77707789 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/app/build.gradle +++ b/dConnectDevicePlugin/dConnectDeviceUVC/app/build.gradle @@ -95,7 +95,7 @@ dependencies { 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.2.2' - implementation 'org.deviceconnect:libsrt:1.2.2' + implementation 'org.deviceconnect:libmedia:1.3.0' + implementation 'org.deviceconnect:libsrt:1.3.0' implementation project(':libuvc') } 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 index 2505fc6cad..018fb2acad 100644 --- 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 @@ -41,7 +41,6 @@ public void start() { MediaRecorder.Settings settings = mRecorder.getSettings(); Size previewSize = settings.getPreviewSize(); setSize(previewSize.getWidth(), previewSize.getHeight()); - setDrawingRange(settings.getDrawingRange()); super.start(); } diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/build.gradle b/dConnectDevicePlugin/dConnectDeviceUVC/build.gradle index 6a0e51ad96..b67c2de44c 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/build.gradle +++ b/dConnectDevicePlugin/dConnectDeviceUVC/build.gradle @@ -6,7 +6,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.2' + classpath 'com.android.tools.build:gradle:4.1.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/dConnectDevicePlugin/dConnectDeviceUVC/gradle/wrapper/gradle-wrapper.properties b/dConnectDevicePlugin/dConnectDeviceUVC/gradle/wrapper/gradle-wrapper.properties index 7a872b986b..09334b7fd7 100644 --- a/dConnectDevicePlugin/dConnectDeviceUVC/gradle/wrapper/gradle-wrapper.properties +++ b/dConnectDevicePlugin/dConnectDeviceUVC/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/dConnectManager/dConnectManager/build.gradle b/dConnectManager/dConnectManager/build.gradle index aa10319315..dbc42e486e 100644 --- a/dConnectManager/dConnectManager/build.gradle +++ b/dConnectManager/dConnectManager/build.gradle @@ -6,7 +6,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.2' + classpath 'com.android.tools.build:gradle:4.1.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/dConnectManager/dConnectManager/dconnect-manager-app-things/build.gradle b/dConnectManager/dConnectManager/dconnect-manager-app-things/build.gradle index 2327ffcc79..1e7c5f616e 100644 --- a/dConnectManager/dConnectManager/dconnect-manager-app-things/build.gradle +++ b/dConnectManager/dConnectManager/dconnect-manager-app-things/build.gradle @@ -38,7 +38,7 @@ android { repositories { maven { name = "DeviceConnect-Android" - url = uri("https://maven.pkg.github.com/DeviceConnect/DeviceConnect-Android") + url = uri("https://maven.pkg.github.com/TakayukiHoshi1984/DeviceConnect-Android") credentials { username = System.getenv("GPR_USER") ?: githubProperties['gpr.usr'] diff --git a/dConnectManager/dConnectManager/dconnect-manager-app/build.gradle b/dConnectManager/dConnectManager/dconnect-manager-app/build.gradle index c26c28191d..671271bfc5 100644 --- a/dConnectManager/dConnectManager/dconnect-manager-app/build.gradle +++ b/dConnectManager/dConnectManager/dconnect-manager-app/build.gradle @@ -82,7 +82,7 @@ android { repositories { maven { name = "DeviceConnect-Android" - url = uri("https://maven.pkg.github.com/DeviceConnect/DeviceConnect-Android") + url = uri("https://maven.pkg.github.com/TakayukiHoshi1984/DeviceConnect-Android") credentials { username = System.getenv("GPR_USER") ?: githubProperties['gpr.usr'] @@ -116,7 +116,7 @@ dependencies { } android.testVariants.all { variant -> - task("generateJavadocForManagerJUnit", type: Javadoc, overwrite: true) { + tasks.register("generate${variant.name.capitalize()}JavadocForManagerJUnit", Javadoc) { title = "Android Device Connect Manager Unit Test" description = "Generates Javadoc for JUnit" source = android.sourceSets.androidTest.java.sourceFiles diff --git a/dConnectManager/dConnectManager/dconnect-manager-app/src/main/java/org/deviceconnect/android/manager/DConnectService.java b/dConnectManager/dConnectManager/dconnect-manager-app/src/main/java/org/deviceconnect/android/manager/DConnectService.java index 0cde35690b..b69f379f7d 100644 --- a/dConnectManager/dConnectManager/dconnect-manager-app/src/main/java/org/deviceconnect/android/manager/DConnectService.java +++ b/dConnectManager/dConnectManager/dconnect-manager-app/src/main/java/org/deviceconnect/android/manager/DConnectService.java @@ -304,41 +304,7 @@ public void onError(final Exception e) { } } } - - /** - * Hostプラグインを追加します. - */ - private void addDevicePlugin() { - String packageName = getPackageName(); - String className = HostDevicePlugin.class.getName(); - - DevicePlugin plugin; - try { - plugin = new DevicePlugin.Builder(this) - .setClassName(className) - .setPackageName(packageName) - .setConnectionType(ConnectionType.DIRECT) - .setDeviceName(getString(R.string.app_name_host)) - .setPluginIconId(R.drawable.dconnect_icon) - .setVersionName(org.deviceconnect.android.deviceplugin.host.BuildConfig.VERSION_NAME) - .setPluginXml(DevicePluginXmlUtil.getXml(getApplicationContext(), - R.xml.org_deviceconnect_android_deviceplugin_host)) - .setPluginId(DConnectUtil.toMD5(packageName + className)) - .setPluginSdkVersionName(VersionName.parse("2.0.0")) - .addProviderAuthority("org.deviceconnect.android.deviceplugin.host.provider.included") - .build(); - mManager.getPluginManager().addDevicePlugin(plugin); - } catch (UnsupportedEncodingException e) { - if (DEBUG) { - Log.e(TAG, "add plugin error.", e); - } - } catch (NoSuchAlgorithmException e) { - if (DEBUG) { - Log.e(TAG, "add plugin error.", e); - } - } - } - /** + /** * DConnectManagerを停止します. */ private void stopManager() { diff --git a/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/DConnectCore.java b/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/DConnectCore.java index 6e671d8f9a..e55192ce54 100644 --- a/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/DConnectCore.java +++ b/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/DConnectCore.java @@ -181,10 +181,6 @@ public void sendMessage(final Intent message) { mPluginManager.addEventListener(new DevicePluginManager.DevicePluginEventListener() { @Override public void onDeviceFound(final DevicePlugin plugin) { - if (mSettings.isRegisterNetworkServiceDiscovery()) { - // 見つけたプラグインを有効にする - plugin.apply(); - } getServiceProvider().addService(plugin); } diff --git a/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/BinderConnection.java b/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/BinderConnection.java index 6d094a7310..c2a58e2119 100755 --- a/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/BinderConnection.java +++ b/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/BinderConnection.java @@ -41,9 +41,9 @@ public class BinderConnection extends AbstractConnection { private Future mRunningTask; - private ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); - private Logger mLogger = Logger.getLogger("dconnect.manager"); + private final Logger mLogger = Logger.getLogger("dconnect.manager"); public BinderConnection(final Context context, final String pluginId, @@ -61,10 +61,10 @@ public ConnectionType getType() { @Override public synchronized void connect() throws ConnectingException { - if (BuildConfig.DEBUG) { - mLogger.info("BinderConnection.connect: " + mPluginName.getPackageName()); - } if (!(ConnectionState.DISCONNECTED == getState() || ConnectionState.SUSPENDED == getState())) { + if (BuildConfig.DEBUG) { + mLogger.info("BinderConnection.connect: state is already connected. plugin=" + mPluginName); + } return; } setConnectingState(); @@ -110,9 +110,6 @@ public void disconnect() { @Override public void send(final Intent message) throws MessagingException { - if (BuildConfig.DEBUG) { - mLogger.info("BinderConnection.send: sending: target = " + mPluginName.getPackageName()); - } synchronized (this) { if (ConnectionState.SUSPENDED == getState()) { throw new MessagingException(MessagingException.Reason.CONNECTION_SUSPENDED); @@ -123,22 +120,14 @@ public void send(final Intent message) throws MessagingException { } try { mPlugin.sendMessage(message); - if (BuildConfig.DEBUG) { - mLogger.info("BinderConnection.send: sent: target = " + mPluginName.getPackageName()); - } } catch (RemoteException e) { throw new MessagingException(e, MessagingException.Reason.NOT_CONNECTED); } } private class ConnectingTask implements Callable { - @Override public ConnectingResult call() throws Exception { - if (BuildConfig.DEBUG) { - mLogger.info("ConnectingTask.call: " + mPluginName); - } - final Object lockObj = new Object(); final ConnectingResult result = new ConnectingResult(); @@ -148,7 +137,9 @@ public ConnectingResult call() throws Exception { @Override public void onServiceConnected(final ComponentName componentName, final IBinder binder) { if (BuildConfig.DEBUG) { - mLogger.info("onServiceConnected: componentName = " + componentName + ", binder = " + binder); + mLogger.info("BinderConnection.onServiceConnected: \n" + + " componentName = " + componentName + "\n" + + " binder = " + binder); } try { IDConnectPlugin plugin = IDConnectPlugin.Stub.asInterface(binder); @@ -160,7 +151,6 @@ public void onServiceConnected(final ComponentName componentName, final IBinder result.mServiceConnection = this; lockObj.notifyAll(); } - } catch (RemoteException e) { e.printStackTrace(); // TODO エラーハンドリング } @@ -169,7 +159,8 @@ public void onServiceConnected(final ComponentName componentName, final IBinder @Override public void onServiceDisconnected(final ComponentName componentName) { if (BuildConfig.DEBUG) { - mLogger.info("onServiceDisconnected: componentName = " + componentName); + mLogger.info("BinderConnection.onServiceDisconnected: " + + " componentName = " + componentName); } synchronized (BinderConnection.this) { mServiceConnection = null; diff --git a/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/DevicePlugin.java b/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/DevicePlugin.java index b51af2f1c2..4bb11c967e 100755 --- a/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/DevicePlugin.java +++ b/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/DevicePlugin.java @@ -191,9 +191,9 @@ public void reportRoundTrip(final Intent request, final long start, final long e // 統計を取る. if (calculatesStats()) { if (BuildConfig.DEBUG) { - mLogger.info("Plug-in PackageName: " + getPackageName()); - mLogger.info("Request: " + DConnectUtil.convertRequestToString(request)); - mLogger.info("ResponseTime: " + (end - start)); + mLogger.info("Plug-in PackageName: " + getPackageName() + "\n" + + " Request: " + DConnectUtil.convertRequestToString(request) + "\n" + + " ResponseTime: " + (end - start)); } long baudRate = info.getRoundTripTime(); @@ -448,9 +448,6 @@ private boolean tryConnection() { for (int cnt = 0; cnt < MAX_CONNECTION_TRY; cnt++) { try { mConnection.connect(); - if (BuildConfig.DEBUG) { - mLogger.info("Connected to the plug-in: " + getPackageName()); - } return true; } catch (ConnectingException e) { mLogger.log(Level.WARNING, "Failed to connect to the plug-in: " + getPackageName(), e); diff --git a/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/DevicePluginManager.java b/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/DevicePluginManager.java index b266ebd7cc..c195ee6fe9 100755 --- a/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/DevicePluginManager.java +++ b/dConnectManager/dConnectManager/dconnect-manager-core/src/main/java/org/deviceconnect/android/manager/core/plugin/DevicePluginManager.java @@ -94,10 +94,8 @@ public class DevicePluginManager { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - mLogger.info("PluginManager: Received: action=" + action); if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { String packageName = getPackageName(intent); - mLogger.info("PluginManager: added package: name=" + packageName); if (packageName != null) { checkAndAddDevicePlugin(packageName); } @@ -112,9 +110,11 @@ public void onReceive(Context context, Intent intent) { private String getPackageName(final Intent intent) { String pkgName = intent.getDataString(); - int idx = pkgName.indexOf(":"); - if (idx != -1) { - pkgName = pkgName.substring(idx + 1); + if (pkgName != null) { + int idx = pkgName.indexOf(":"); + if (idx != -1) { + pkgName = pkgName.substring(idx + 1); + } } return pkgName; } @@ -137,7 +137,7 @@ private String getPackageName(final Intent intent) { /** * イベントを通知するスレッド. */ - private ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); /** * コンテキスト. @@ -152,13 +152,10 @@ private String getPackageName(final Intent intent) { /** * 接続管理用インスタンスのイベントリスナー. */ - private final ConnectionStateListener mStateListener = new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(final String pluginId, final ConnectionState state) { - DevicePlugin plugin = mPlugins.get(pluginId); - if (plugin != null) { - notifyStateChange(plugin, state); - } + private final ConnectionStateListener mStateListener = (pluginId, state) -> { + DevicePlugin plugin = mPlugins.get(pluginId); + if (plugin != null) { + notifyStateChange(plugin, state); } }; @@ -185,10 +182,10 @@ public void startMonitoring() { packageFilter.addDataScheme("package"); try { mContext.registerReceiver(mPackageReceiver, packageFilter); - mLogger.info("PluginManager: Started plugin monitoring."); } catch (Exception e) { - // ignore. - mLogger.severe("PluginManager: Failed to start plugin monitoring: " + e.getMessage()); + if (BuildConfig.DEBUG) { + mLogger.severe("PluginManager: Failed to start plugin monitoring: " + e.getMessage()); + } } } @@ -198,12 +195,11 @@ public void startMonitoring() { public void stopMonitoring() { try { mContext.unregisterReceiver(mPackageReceiver); - mLogger.info("PluginManager: Stopped plugin monitoring."); } catch (Exception e) { - // ignore. - mLogger.severe("PluginManager: Failed to stop plugin monitoring: " + e.getMessage()); + if (BuildConfig.DEBUG) { + mLogger.severe("PluginManager: Failed to stop plugin monitoring: " + e.getMessage()); + } } - } /** @@ -244,12 +240,13 @@ public void addDevicePlugin(final DevicePlugin plugin) { if (mConnectionFactory != null) { plugin.setConnection(mConnectionFactory.createConnectionForPlugin(plugin)); plugin.addConnectionStateListener(mStateListener); - mLogger.info("PluginManager: created connection to plugin: package=" + plugin.getPackageName()); } else { - mLogger.info("PluginManager: No connection factory: package=" + plugin.getPackageName()); + if (BuildConfig.DEBUG) { + mLogger.info("PluginManager: No connection factory: package=" + plugin.getPackageName()); + } } mPlugins.put(plugin.getPluginId(), plugin); - mLogger.info("PluginManager: added to plugin list: package=" + plugin.getPackageName() + ", ID=" + plugin.getPluginId()); + plugin.apply(); notifyFound(plugin); } @@ -266,7 +263,7 @@ public void createDevicePluginList() throws PluginDetectionException { allPlugins = getInstalledPlugins(pkgMgr); } catch (Exception e) { PluginDetectionException.Reason reason; - if (Build.VERSION.SDK_INT >= 15) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { if (e.getClass() == TransactionTooLargeException.class) { reason = PluginDetectionException.Reason.TOO_MANY_PACKAGES; } else { @@ -425,10 +422,8 @@ private void checkAndAddDevicePlugin(final String packageName) { try { int flag = PackageManager.GET_SERVICES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS; PackageInfo pkg = pkgMgr.getPackageInfo(packageName, flag); - mLogger.info("PluginManager: get package info: " + pkg); if (pkg != null) { List plugins = getInstalledPluginsForPackage(pkgMgr, pkg); - mLogger.info("PluginManager: installed plugins: size=" + plugins.size()); for (DevicePlugin plugin : filterPlugin(plugins)) { addDevicePlugin(plugin); } @@ -461,25 +456,24 @@ private List filterPlugin(final List plugins) { for (int index = 0; index < array.size(); index++) { List list = array.valueAt(index); if (list != null && list.size() > 0) { - Collections.sort(list, new Comparator() { - @Override - public int compare(final DevicePlugin p1, final DevicePlugin p2) { - // BroadcastよりもBinderを優先する. - if (p1.getConnectionType() == p2.getConnectionType()) { - return 0; - } else if (p1.getConnectionType() == ConnectionType.BINDER) { - return -1; - } else { - return 1; - } - } - }); + Collections.sort(list, COMPARATOR); result.add(list.get(0)); } } return result; } + private static final Comparator COMPARATOR = (Comparator) (p1, p2) -> { + // BroadcastよりもBinderを優先する. + if (p1.getConnectionType() == p2.getConnectionType()) { + return 0; + } else if (p1.getConnectionType() == ConnectionType.BINDER) { + return -1; + } else { + return 1; + } + }; + /** * 指定されたコンポーネントの ServiceInfo を取得します. *

@@ -567,17 +561,17 @@ private DevicePlugin parsePlugin(final PackageInfo pkgInfo, final ComponentInfo Integer iconId = (Integer) metaData.get(PLUGIN_META_PLUGIN_ICON); if (BuildConfig.DEBUG) { - mLogger.info("Added DevicePlugin: [" + hash + "]"); - mLogger.info(" PackageName: " + packageName); - mLogger.info(" className: " + className); - mLogger.info(" versionName: " + versionName); - mLogger.info(" sdkVersionName: " + sdkVersionName); - } - - // MEMO 既に同じ名前のデバイスプラグインが存在した場合の処理 - // 現在は警告を表示し、上書きする. - if (mPlugins.containsKey(hash)) { - mLogger.warning("DevicePlugin[" + hash + "] already exists."); + mLogger.info("Added DevicePlugin: [" + hash + "]\n" + + " PackageName: " + packageName + "\n" + + " className: " + className + "\n" + + " versionName: " + versionName + "\n" + + " sdkVersionName: " + sdkVersionName); + + // MEMO 既に同じ名前のデバイスプラグインが存在した場合の処理 + // 現在は警告を表示し、上書きする. + if (mPlugins.containsKey(hash)) { + mLogger.warning("DevicePlugin[" + hash + "] already exists."); + } } ConnectionType type; @@ -779,10 +773,12 @@ public void appendPluginIdToSessionKey(final Intent request, final DevicePlugin */ public void splitPluginIdToServiceId(final Intent request) { String serviceId = request.getStringExtra(DConnectMessage.EXTRA_SERVICE_ID); - List plugins = getDevicePlugins(serviceId); - // 各デバイスプラグインへ渡すサービスIDを作成 - String id = DevicePluginManager.splitServiceId(plugins.get(0), serviceId); - request.putExtra(IntentDConnectMessage.EXTRA_SERVICE_ID, id); + if (serviceId != null) { + List plugins = getDevicePlugins(serviceId); + // 各デバイスプラグインへ渡すサービスIDを作成 + String id = DevicePluginManager.splitServiceId(plugins.get(0), serviceId); + request.putExtra(IntentDConnectMessage.EXTRA_SERVICE_ID, id); + } } /** diff --git a/dConnectManager/dConnectManager/gradle/wrapper/gradle-wrapper.properties b/dConnectManager/dConnectManager/gradle/wrapper/gradle-wrapper.properties index 693811bef3..3e80d63286 100644 --- a/dConnectManager/dConnectManager/gradle/wrapper/gradle-wrapper.properties +++ b/dConnectManager/dConnectManager/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/build.gradle b/dConnectSDK/dConnectLibStreaming/libmedia/build.gradle index 511a83b1f1..4b08475163 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/build.gradle +++ b/dConnectSDK/dConnectLibStreaming/libmedia/build.gradle @@ -8,7 +8,7 @@ if (githubPropertiesFile.exists()) { } def getVersionName = { -> - return "1.2.2" + return "1.3.0" } def getArtificatId = { -> @@ -16,7 +16,7 @@ def getArtificatId = { -> } android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 21 diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/EGLSurfaceBase.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/EGLSurfaceBase.java index ed04bc270c..851297ef6c 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/EGLSurfaceBase.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/EGLSurfaceBase.java @@ -1,5 +1,6 @@ package org.deviceconnect.android.libmedia.streaming.gles; +import android.graphics.Rect; import android.opengl.EGL14; import android.opengl.EGLSurface; import android.opengl.GLES20; @@ -15,6 +16,7 @@ public abstract class EGLSurfaceBase { private int mWidth = -1; private int mHeight = -1; private Object mTag; + private Rect mCropRect; EGLSurfaceBase() { } @@ -83,6 +85,26 @@ public int getHeight() { } } + /** + * 描画範囲を設定します. + * + * @param rect 描画範囲 + */ + public void setCropRect(Rect rect) { + mCropRect = rect; + } + + /** + * 描画範囲を取得します. + * + * 未設定の場合には null を返却します。 + * + * @return 描画範囲 + */ + public Rect getCropRect() { + return mCropRect; + } + /** * Discards all resources held by this class, notably the EGL context. Also releases the * Surface that was passed to our constructor. diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/EGLSurfaceDrawingThread.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/EGLSurfaceDrawingThread.java index 583e6ade91..03c676034c 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/EGLSurfaceDrawingThread.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/EGLSurfaceDrawingThread.java @@ -38,7 +38,7 @@ public class EGLSurfaceDrawingThread { /** * レンダリングのタイムアウト(ミリ秒). */ - private int mTimeout = 10 * 1000; + private int mRenderingTimeout = 10 * 1000; /** * イベントを通知するリスナー. @@ -50,11 +50,6 @@ public class EGLSurfaceDrawingThread { */ private DrawingThread mDrawingThread; - /** - * 描画範囲. - */ - private Rect mDrawingRange; - /** * イベントを通知するリスナーを追加します. * @@ -87,11 +82,11 @@ public void removeOnDrawingEventListener(OnDrawingEventListener listener) { * * @param timeout タイムアウト */ - public void setTimeout(int timeout) { + public void setRenderingTimeout(int timeout) { if (timeout < 0) { throw new IllegalArgumentException("timeout cannot set negative value."); } - mTimeout = timeout; + mRenderingTimeout = timeout; } /** @@ -99,30 +94,15 @@ public void setTimeout(int timeout) { * * @return タイムアウト */ - public int getTimeout() { - return mTimeout; + public int getRenderingTimeout() { + return mRenderingTimeout; } /** - * 描画範囲を設定します. + * カメラなどの映像ソースのサイズを設定します. * - * @param rect 描画範囲 - */ - public void setDrawingRange(Rect rect) { - mDrawingRange = rect; - } - - /** - * 描画範囲設定を取得します. - * - * @return 描画範囲 - */ - public Rect getDrawingRange() { - return mDrawingRange; - } - - /** - * 描画を行う Surface のサイズを設定します. + * 映像ソースのサイズと配信先のサイズから映像が崩れないように + * アスペクト比を計算を行い描画を行います。 * * @param width 横幅 * @param height 縦幅 @@ -196,19 +176,35 @@ public void addEGLSurfaceBase(EGLSurfaceBase eglSurfaceBase) { * @param surface 追加する EGLSurfaceBase に設定する Surface */ public void addEGLSurfaceBase(Surface surface) { - addEGLSurfaceBase(surface, surface); + addEGLSurfaceBase(surface, surface, null); } /** * 描画先の EGLSurfaceBase を追加します. * * 引数に設定した Surface の EGLSurfaceBase を作成します。 + * また、タグには、引数に指定された surface を設定します。 + * + * 作成した EGLSurfaceBase に引数で指定された drawingRange を描画範囲に設定します。 * * @param surface 追加する EGLSurfaceBase に設定する Surface + * @param drawingRange 描画範囲 */ - public void addEGLSurfaceBase(Surface surface, Object tag) { + public void addEGLSurfaceBase(Surface surface, Rect drawingRange) { + addEGLSurfaceBase(surface, surface, drawingRange); + } + + /** + * 描画先の EGLSurfaceBase を追加します. + * + * 引数に設定した Surface の EGLSurfaceBase を作成します。 + * + * @param surface 追加する EGLSurfaceBase に設定する Surface + */ + public void addEGLSurfaceBase(Surface surface, Object tag, Rect drawingRange) { EGLSurfaceBase eglSurfaceBase = createEGLSurfaceBase(surface); eglSurfaceBase.setTag(tag); + eglSurfaceBase.setCropRect(drawingRange); addEGLSurfaceBase(eglSurfaceBase); } @@ -224,6 +220,22 @@ public void addEGLSurfaceBase(Surface surface, Object tag) { public void addEGLSurfaceBase(int width, int height, Object tag) { EGLSurfaceBase eglSurfaceBase = createEGLSurfaceBase(width, height); eglSurfaceBase.setTag(tag); + addEGLSurfaceBase(width, height, tag, null); + } + + /** + * 描画先の EGLSurfaceBase を追加します. + * + * 引数に指定された width と height でオフスクリーンの EGLSurfaceBase を作成します。 + * + * @param width 横幅 + * @param height 縦幅 + * @param tag タグ + */ + public void addEGLSurfaceBase(int width, int height, Object tag, Rect drawingRange) { + EGLSurfaceBase eglSurfaceBase = createEGLSurfaceBase(width, height); + eglSurfaceBase.setTag(tag); + eglSurfaceBase.setCropRect(drawingRange); addEGLSurfaceBase(eglSurfaceBase); } @@ -371,9 +383,6 @@ protected SurfaceTextureManager createStManager() { SurfaceTextureManager manager = new SurfaceTextureManager(); SurfaceTexture st = manager.getSurfaceTexture(); st.setDefaultBufferSize(mWidth, mHeight); - if (mDrawingRange != null) { - manager.setDrawingRange(mDrawingRange, mWidth, mHeight); - } return manager; } @@ -465,7 +474,11 @@ protected void postOnError(Exception e) { private void postOnDrawn(EGLSurfaceBase eglSurfaceBase) { for (OnDrawingEventListener l : mOnDrawingEventListeners) { - l.onDrawn(eglSurfaceBase); + try { + l.onDrawn(eglSurfaceBase); + } catch (Exception e) { + // ignore. + } } } @@ -523,7 +536,7 @@ public void run() { mEGLCore.makeCurrent(); mStManager = createStManager(); - mStManager.setTimeout(mTimeout); + mStManager.setTimeout(mRenderingTimeout); synchronized (mEGLSurfaceBases) { for (EGLSurfaceBase surfaceBase : mEGLSurfaceBases) { @@ -536,6 +549,7 @@ public void run() { onStarted(); postOnStarted(); + ViewSize size = new ViewSize(); SurfaceTexture st = mStManager.getSurfaceTexture(); while (mState == STATE_RUNNING) { mStManager.awaitNewImage(); @@ -543,7 +557,26 @@ public void run() { synchronized (mEGLSurfaceBases) { for (EGLSurfaceBase eglSurfaceBase : mEGLSurfaceBases) { eglSurfaceBase.makeCurrent(); - mStManager.setViewport(0, 0, eglSurfaceBase.getWidth(), eglSurfaceBase.getHeight()); + int viewportX = 0; + int viewportY = 0; + int viewportW = eglSurfaceBase.getWidth(); + int viewportH = eglSurfaceBase.getHeight(); + Rect cropRect = eglSurfaceBase.getCropRect(); + if (cropRect != null) { + mStManager.setCropRect(cropRect, mWidth, mHeight); + calculateViewSize(cropRect.width(), cropRect.height(), viewportW, viewportH, size); + } else { + mStManager.clearCropRect(); + calculateViewSize(mWidth, mHeight, viewportW, viewportH, size); + } + if (viewportW > size.getWidth()) { + viewportX = (viewportW - size.getWidth()) / 2; + viewportW = size.getWidth(); + } else if (viewportH > size.getHeight()) { + viewportY = (viewportH - size.getHeight()) / 2; + viewportH = size.getHeight(); + } + mStManager.setViewport(viewportX, viewportY,viewportW, viewportH); mStManager.drawImage(getDisplayRotation()); eglSurfaceBase.setPresentationTime(st.getTimestamp()); eglSurfaceBase.swapBuffers(); @@ -586,6 +619,46 @@ public void run() { } } + private static class ViewSize { + int mWidth; + int mHeight; + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + } + + /** + * 指定された View のサイズにフィットするサイズを計算します. + * + * @param width 横幅 + * @param height 縦幅 + * @param viewWidth View のサイズ + * @param viewHeight View のサイズ + * @param dest 出力先 + */ + private void calculateViewSize(int width, int height, int viewWidth, int viewHeight, ViewSize dest) { + int h = (int) (height * (viewWidth / (float) width)); + if (viewHeight < h) { + int w = (int) (width * (viewHeight / (float) height)); + if (w % 2 != 0) { + w--; + } + dest.mWidth = w; + dest.mHeight = viewHeight; + } else { + dest.mWidth = viewWidth; + dest.mHeight = h; + } + } + + /** + * 描画イベントを通知するリスナー. + */ public interface OnDrawingEventListener { /** * 描画の開始を通知します. diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/SurfaceTextureManager.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/SurfaceTextureManager.java index 3ce60e0855..1d1bc33218 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/SurfaceTextureManager.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/SurfaceTextureManager.java @@ -145,8 +145,8 @@ public void setViewport(int x, int y, int width, int height) { * @param width 映像の横幅 * @param height 映像の縦幅 */ - public void setDrawingRange(int l, int t, int r, int b, int width, int height) { - mTextureRenderer.setDrawingRange(l, t, r, b, width, height); + public void setCropRect(int l, int t, int r, int b, int width, int height) { + mTextureRenderer.setCropRect(l, t, r, b, width, height); } /** @@ -156,8 +156,15 @@ public void setDrawingRange(int l, int t, int r, int b, int width, int height) { * @param width 映像の横幅 * @param height 映像の縦幅 */ - public void setDrawingRange(Rect rect, int width, int height) { - mTextureRenderer.setDrawingRange(rect, width, height); + public void setCropRect(Rect rect, int width, int height) { + mTextureRenderer.setCropRect(rect, width, height); + } + + /** + * 描画範囲を解除します. + */ + public void clearCropRect() { + mTextureRenderer.clearCropRect(); } /** diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/SurfaceTextureRenderer.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/SurfaceTextureRenderer.java index 2c97dab7d2..84eaaeb9cc 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/SurfaceTextureRenderer.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/gles/SurfaceTextureRenderer.java @@ -39,6 +39,7 @@ public class SurfaceTextureRenderer { }; private final FloatBuffer mTriangleVertices; + private final boolean mInverse; private static final String VERTEX_SHADER = "uniform mat4 uMVPMatrix;\n" + @@ -76,7 +77,8 @@ public class SurfaceTextureRenderer { * コンストラクタ. * @param inverse テクスチャの反転フラグ */ - SurfaceTextureRenderer(boolean inverse) { + public SurfaceTextureRenderer(boolean inverse) { + mInverse = inverse; mTriangleVertices = ByteBuffer.allocateDirect( TRIANGLE_VERTICES_DATA.length * FLOAT_SIZE_BYTES) .order(ByteOrder.nativeOrder()).asFloatBuffer(); @@ -220,8 +222,27 @@ public void drawFrame(SurfaceTexture st, int displayRotation) { GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0); } - public void setDrawingRange(Rect rect, int width, int height) { - setDrawingRange(rect.left, rect.top, rect.right, rect.bottom, width, height); + /** + * 描画範囲を削除します. + */ + public void clearCropRect() { + mTriangleVertices.clear(); + if (mInverse) { + mTriangleVertices.put(TRIANGLE_VERTICES_DATA_2).position(0); + } else { + mTriangleVertices.put(TRIANGLE_VERTICES_DATA).position(0); + } + } + + /** + * 描画範囲を設定します. + * + * @param rect 範囲 + * @param width 描画元の横幅 + * @param height 描画元の縦幅 + */ + public void setCropRect(Rect rect, int width, int height) { + setCropRect(rect.left, rect.top, rect.right, rect.bottom, width, height); } /** @@ -234,15 +255,15 @@ public void setDrawingRange(Rect rect, int width, int height) { * @param width 画像の横幅 * @param height 画像の縦幅 */ - public void setDrawingRange(int left, int top, int right, int bottom, int width, int height) { + public void setCropRect(int left, int top, int right, int bottom, int width, int height) { float l = left / (float) width; float t = top / (float) height; float r = right / (float) width; float b = bottom / (float) height; - setDrawingRange(l, t, r, b); + setCropRect(l, t, r, b); } - private void setDrawingRange(float l, float t, float r, float b) { + private void setCropRect(float l, float t, float r, float b) { // 映像がはみ出した分は縮めて描画するように座標を計算 float sx = -1.0f; float sy = 1.0f; @@ -256,6 +277,14 @@ private void setDrawingRange(float l, float t, float r, float b) { ey = -2.0f / b + 1.0f; b = 1.0f; } + if (l < 0.0f) { + sx = sx - l; + l = 0.0f; + } + if (t < 0.0f) { + sy = sy - t; + t = 0.0f; + } float[] triangleVerticesData = { // X, Y, Z, U, V sx, sy, 0.f, l, (1 - t), diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/mjpeg/MJPEGQuality.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/mjpeg/MJPEGQuality.java index ee6c546062..24e502b550 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/mjpeg/MJPEGQuality.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/mjpeg/MJPEGQuality.java @@ -1,5 +1,6 @@ package org.deviceconnect.android.libmedia.streaming.mjpeg; +import android.graphics.Rect; import android.hardware.camera2.CameraCharacteristics; public class MJPEGQuality { @@ -8,6 +9,7 @@ public class MJPEGQuality { private int mHeight = 640; private int mQuality = 60; private int mFrameRate = 30; + private Rect mCropRect; /** * Motion JPEG の横幅を取得します. @@ -80,6 +82,15 @@ public int getFrameRate() { return mFrameRate; } + /** + * フレームレートをミリ秒に変換して取得します. + * + * @return フレームレート(ミリ秒) + */ + public int getFrameRateMSEC() { + return 1000 / mFrameRate; + } + /** * フレームレートを設定します. * @@ -89,11 +100,41 @@ public void setFrameRate(int frameRate) { mFrameRate = frameRate; } + /** + * カメラの向きを取得します. + * + * @return カメラの向き + */ public int getFacing() { return mFacing; } + /** + * カメラの向きを設定します. + * + * @param facing カメラの向き + */ public void setFacing(int facing) { mFacing = facing; } + + /** + * 描画範囲を設定します. + * + * @param cropRect 描画範囲 + */ + public void setCropRect(Rect cropRect) { + mCropRect = cropRect; + } + + /** + * 描画範囲を取得します. + * + * 描画範囲が全体の場合には null が返却されます。 + * + * @return 描画範囲 + */ + public Rect getCropRect() { + return mCropRect; + } } diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/mjpeg/SurfaceMJPEGEncoder.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/mjpeg/SurfaceMJPEGEncoder.java index 866dac64b7..35b47d380f 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/mjpeg/SurfaceMJPEGEncoder.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/mjpeg/SurfaceMJPEGEncoder.java @@ -60,6 +60,11 @@ public abstract class SurfaceMJPEGEncoder extends MJPEGEncoder { */ private EncoderThread mEncoderThread; + /** + * JPEG の送信時間を格納する. + */ + private long mSendTime; + /** * EGLSurfaceDrawingThread のイベントを受け取るリスナー. */ @@ -88,6 +93,11 @@ public void onError(Exception e) { @Override public void onDrawn(EGLSurfaceBase eglSurfaceBase) { if (TAG_SURFACE.equals(eglSurfaceBase.getTag())) { + MJPEGQuality mjpegQuality = getMJPEGQuality(); + if (mjpegQuality != null) { + eglSurfaceBase.setCropRect(mjpegQuality.getCropRect()); + } + try { drainEncoder(eglSurfaceBase, eglSurfaceBase.getWidth(), eglSurfaceBase.getHeight()); } catch (Throwable t) { @@ -165,8 +175,6 @@ protected EGLSurfaceDrawingThread createEGLSurfaceDrawingThread() { */ private synchronized void startDrawingThreadInternal() { MJPEGQuality quality = getMJPEGQuality(); - int w = quality.getWidth(); - int h = quality.getHeight(); if (mEncoderThread != null) { mEncoderThread.terminate(); @@ -179,8 +187,9 @@ private synchronized void startDrawingThreadInternal() { if (mInternalCreateSurfaceDrawingThread) { mSurfaceDrawingThread = createEGLSurfaceDrawingThread(); } - mSurfaceDrawingThread.setSize(w, h); - mSurfaceDrawingThread.addEGLSurfaceBase(quality.getWidth(), quality.getHeight(), TAG_SURFACE); + + mSurfaceDrawingThread.setSize(quality.getWidth(), quality.getHeight()); + mSurfaceDrawingThread.addEGLSurfaceBase(quality.getWidth(), quality.getHeight(), TAG_SURFACE, quality.getCropRect()); mSurfaceDrawingThread.addOnDrawingEventListener(mOnDrawingEventListener); mSurfaceDrawingThread.start(); } @@ -216,10 +225,16 @@ private synchronized void stopDrawingThreadInternal() { * @param height 映像の縦幅 */ private void drainEncoder(EGLSurfaceBase surface, int width, int height) { + long nowTime = System.currentTimeMillis(); + if (nowTime - mSendTime < getMJPEGQuality().getFrameRateMSEC()) { + return; + } + if (mEncodeFlag || mEncoderThread == null) { return; } mEncodeFlag = true; + mSendTime = nowTime; // OpenGLES からピクセルデータを取得するバッファを作成 if (mBuffer == null || width != mBitmap.getWidth() || height != mBitmap.getHeight()) { diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/muxer/RtmpMuxer.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/muxer/RtmpMuxer.java index 24ae37cb43..352b53b681 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/muxer/RtmpMuxer.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/muxer/RtmpMuxer.java @@ -113,6 +113,7 @@ public void onAuthErrorRtmp() { }; mSrsFlvMuxer = new SrsFlvMuxer(rtmpAdapter); + mSrsFlvMuxer.setLogs(false); if (videoQuality != null) { mSrsFlvMuxer.setVideoResolution(videoQuality.getVideoWidth(), videoQuality.getVideoHeight()); } diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtp/packet/H264Packetize.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtp/packet/H264Packetize.java index b4ecbe88e9..5bbb15b276 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtp/packet/H264Packetize.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtp/packet/H264Packetize.java @@ -56,10 +56,18 @@ public void write(byte[] data, int dataLength, long pts) { pts = updateTimestamp(pts * 1000L); - if (dataLength <= MAX_PACKET_SIZE - HEADER_LEN) { - writeSingleNALUnit(data, dataLength, pts); - } else { - writeFragmentationUnits(data, dataLength, pts); + int start = 0; + while (start < dataLength) { + int end = searchNalUnit(data, start + 4, dataLength); + int length = end - start; + + if (length <= MAX_PACKET_SIZE - HEADER_LEN) { + writeSingleNALUnit(data, start, length, pts); + } else { + writeFragmentationUnits(data, start, length, pts); + } + + start += length; } } @@ -76,14 +84,14 @@ public void write(byte[] data, int dataLength, long pts) { // | :...OPTIONAL RTP padding | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - private void writeSingleNALUnit(byte[] data, int dataLength, long pts) { + private void writeSingleNALUnit(byte[] data, int offset, int dataLength, long pts) { RtpPacket rtpPacket = getRtpPacket(); byte[] dest = rtpPacket.getBuffer(); writeRtpHeader(dest, pts); writeNextPacket(dest); - System.arraycopy(data, 4, dest, RTP_HEADER_LENGTH, dataLength - 4); + System.arraycopy(data, offset + 4, dest, RTP_HEADER_LENGTH, dataLength - 4); send(rtpPacket, RTP_HEADER_LENGTH + dataLength - 4, pts); } @@ -122,10 +130,10 @@ private void writeSingleNALUnit(byte[] data, int dataLength, long pts) { //|S|E|R| Type | //+---------------+ - private void writeFragmentationUnits(byte[] data, int dataLength, long pts) { + private void writeFragmentationUnits(byte[] data, int srcOffset, int dataLength, long pts) { dataLength -= 5; - int offset = 5; + int offset = srcOffset + 5; while (dataLength > 0) { RtpPacket rtpPacket = getRtpPacket(); byte[] dest = rtpPacket.getBuffer(); @@ -133,12 +141,12 @@ private void writeFragmentationUnits(byte[] data, int dataLength, long pts) { writeRtpHeader(dest, pts); // Set FU-A indicator - dest[RTP_HEADER_LENGTH] = (byte) (data[4] & 0xE0); + dest[RTP_HEADER_LENGTH] = (byte) (data[srcOffset + 4] & 0xE0); dest[RTP_HEADER_LENGTH] += 28; // (FU-A) // Set FU-A header - dest[RTP_HEADER_LENGTH + 1] = (byte) (data[4] & 0x1F); - if (offset == 5) { + dest[RTP_HEADER_LENGTH + 1] = (byte) (data[srcOffset + 4] & 0x1F); + if (offset == srcOffset + 5) { // set start flag dest[RTP_HEADER_LENGTH + 1] += 0x80; } @@ -161,4 +169,20 @@ private void writeFragmentationUnits(byte[] data, int dataLength, long pts) { dataLength -= length; } } + + private int searchNalUnit(byte[] data, int offset, int dataLength) { + // NALU_TYPE_SEI (6) よりも小さい nalu type の場合は、後ろに nalu unit が + // 存在する端末がなかったので、ここでは最後までのデータサイズを返却するようにする。 + int naluType = data[offset] & 0x1F; + if (naluType < 6) { + return dataLength; + } + + for (int i = offset; i < dataLength - 4; i++) { + if (data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x00 && data[i + 3] == 0x01) { + return i; + } + } + return dataLength; + } } diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtp/packet/H265Packetize.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtp/packet/H265Packetize.java index e204239469..82f34b7a83 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtp/packet/H265Packetize.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtp/packet/H265Packetize.java @@ -31,10 +31,18 @@ public void write(byte[] data, int dataLength, long pts) { pts = updateTimestamp(pts * 1000L); - if (dataLength <= MAX_PACKET_SIZE - HEADER_LEN) { - writeSingleNALUnit(data, dataLength, pts); - } else { - writeFragmentationUnits(data, dataLength, pts); + int start = 0; + while (start < dataLength) { + int end = searchNalUnit(data, start + 4, dataLength); + int length = end - start; + + if (length <= MAX_PACKET_SIZE - HEADER_LEN) { + writeSingleNALUnit(data, start, length, pts); + } else { + writeFragmentationUnits(data, start, length, pts); + } + + start += length; } } @@ -51,14 +59,14 @@ public void write(byte[] data, int dataLength, long pts) { // | :...OPTIONAL RTP padding | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - private void writeSingleNALUnit(byte[] data, int dataLength, long pts) { + private void writeSingleNALUnit(byte[] data, int offset, int dataLength, long pts) { RtpPacket rtpPacket = getRtpPacket(); byte[] dest = rtpPacket.getBuffer(); writeRtpHeader(dest, pts); writeNextPacket(dest); - System.arraycopy(data, 4, dest, RTP_HEADER_LENGTH, dataLength - 4); + System.arraycopy(data, offset + 4, dest, RTP_HEADER_LENGTH, dataLength - 4); send(rtpPacket, RTP_HEADER_LENGTH + dataLength - 4, pts); } @@ -91,12 +99,12 @@ private void writeSingleNALUnit(byte[] data, int dataLength, long pts) { // |S|E| FuType | // +---------------+ - private void writeFragmentationUnits(byte[] data, int dataLength, long pts) { - byte naluType = (byte) ((data[4] >> 1) & 0x3F); + private void writeFragmentationUnits(byte[] data, int srcOffset, int dataLength, long pts) { + byte naluType = (byte) ((data[srcOffset + 4] >> 1) & 0x3F); dataLength -= 6; - int offset = 6; + int offset = srcOffset + 6; while (dataLength > 0) { RtpPacket rtpPacket = getRtpPacket(); byte[] dest = rtpPacket.getBuffer(); @@ -106,7 +114,7 @@ private void writeFragmentationUnits(byte[] data, int dataLength, long pts) { dest[RTP_HEADER_LENGTH] = 49 << 1; dest[RTP_HEADER_LENGTH + 1] = 1; dest[RTP_HEADER_LENGTH + 2] = naluType; - if (offset == 6) { + if (offset == srcOffset + 6) { // set start flag dest[RTP_HEADER_LENGTH + 2] += 0x80; } @@ -129,4 +137,20 @@ private void writeFragmentationUnits(byte[] data, int dataLength, long pts) { dataLength -= length; } } + + private int searchNalUnit(byte[] data, int offset, int dataLength) { + // NAL_UNIT_VPS (32) よりも小さい nalu type の場合は、後ろに nalu unit が + // 存在する端末がなかったので、ここでは最後までのデータサイズを返却するようにする。 + int naluType = (data[offset] >> 1) & 0x3F; + if (naluType < 32) { + return dataLength; + } + + for (int i = offset; i < dataLength - 4; i++) { + if (data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x00 && data[i + 3] == 0x01) { + return i; + } + } + return dataLength; + } } diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtsp/session/RtspSession.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtsp/session/RtspSession.java index e3e0b1c368..271ec90c4c 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtsp/session/RtspSession.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtsp/session/RtspSession.java @@ -296,22 +296,24 @@ public void onWriteVideoData(ByteBuffer encodedData, MediaCodec.BufferInfo buffe encodedData.limit(bufferInfo.offset + bufferInfo.size); encodedData.get(mConfigData, 0, bufferInfo.size); mConfigLength = bufferInfo.size; - } - boolean isKeyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0; - if (isKeyFrame && mConfigData != null) { - // H264 の SPS、PPS はキーフレームごとに送信するようにする。 videoStream.writePacket(mConfigData, mConfigLength, pts); - } + } else { + boolean isKeyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0; + if (isKeyFrame && mConfigData != null) { + // H264 の SPS、PPS はキーフレームごとに送信するようにする。 + videoStream.writePacket(mConfigData, mConfigLength, pts); + } - if (mVideoBuffer.length < bufferInfo.size) { - mVideoBuffer = new byte[bufferInfo.size]; - } - encodedData.position(bufferInfo.offset); - encodedData.limit(bufferInfo.offset + bufferInfo.size); - encodedData.get(mVideoBuffer, 0, bufferInfo.size); + if (mVideoBuffer.length < bufferInfo.size) { + mVideoBuffer = new byte[bufferInfo.size]; + } + encodedData.position(bufferInfo.offset); + encodedData.limit(bufferInfo.offset + bufferInfo.size); + encodedData.get(mVideoBuffer, 0, bufferInfo.size); - videoStream.writePacket(mVideoBuffer, bufferInfo.size, pts); + videoStream.writePacket(mVideoBuffer, bufferInfo.size, pts); + } } } diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtsp/session/audio/AudioStream.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtsp/session/audio/AudioStream.java index 6b26af3036..9d693afa66 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtsp/session/audio/AudioStream.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/rtsp/session/audio/AudioStream.java @@ -7,7 +7,7 @@ public abstract class AudioStream extends MediaStream { /** * 音声を配信するポート番号を定義します. */ - private static final int AUDIO_PORT = 5006; + private static final int AUDIO_PORT = 5004; public AudioStream() { setDestinationPort(AUDIO_PORT); diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CameraSurfaceVideoEncoder.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CameraSurfaceVideoEncoder.java index b96360b13b..78c4efd4e2 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CameraSurfaceVideoEncoder.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CameraSurfaceVideoEncoder.java @@ -62,14 +62,4 @@ public CameraSurfaceVideoEncoder(CameraVideoQuality videoQuality, EGLSurfaceDraw public VideoQuality getVideoQuality() { return mVideoQuality; } - - // SurfaceVideoEncoder - - @Override - protected void onStartSurfaceDrawing() { - } - - @Override - protected void onStopSurfaceDrawing() { - } } diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CameraVideoQuality.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CameraVideoQuality.java index a63a7e7d86..412b1268db 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CameraVideoQuality.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CameraVideoQuality.java @@ -1,6 +1,7 @@ package org.deviceconnect.android.libmedia.streaming.video; import android.hardware.camera2.CameraCharacteristics; +import android.util.Range; public class CameraVideoQuality extends VideoQuality { /** @@ -8,6 +9,11 @@ public class CameraVideoQuality extends VideoQuality { */ private int mFacing = CameraCharacteristics.LENS_FACING_BACK; + /** + * カメラのFPS. + */ + private Range mFps; + /** * コンストラクタ. * @param mimeType マイムタイプ @@ -24,6 +30,9 @@ public CameraVideoQuality(String mimeType) { public void set(CameraVideoQuality quality) { super.set(quality); mFacing = quality.getFacing(); + if (quality.mFps != null) { + mFps = new Range<>(quality.mFps.getLower(), quality.mFps.getUpper()); + } } /** @@ -52,4 +61,24 @@ public int getFacing() { public void setFacing(int facing) { mFacing = facing; } + + /** + * カメラの fps を取得します. + * + * 設定されていない場合は null を返却します. + * + * @return fps + */ + public Range getFps() { + return mFps; + } + + /** + * カメラの fps を設定します. + * + * @param fps カメラの FPS + */ + public void setFps(Range fps) { + mFps = fps; + } } diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CanvasVideoEncoder.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CanvasVideoEncoder.java index e65e00fb14..d30a14c16d 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CanvasVideoEncoder.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/CanvasVideoEncoder.java @@ -131,7 +131,7 @@ private void terminate() { interrupt(); try { - join(200); + join(500); } catch (InterruptedException e) { // ignore. } diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/SurfaceVideoEncoder.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/SurfaceVideoEncoder.java index 52c88cf742..27ad190ffc 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/SurfaceVideoEncoder.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/SurfaceVideoEncoder.java @@ -29,6 +29,11 @@ public abstract class SurfaceVideoEncoder extends VideoEncoder { */ private final boolean mInternalCreateSurfaceDrawingThread; + /** + * 描画先の EGLSurfaceBase. + */ + private EGLSurfaceBase mEGLSurfaceBase; + /** * EGLSurfaceDrawingThread からのイベントを受け取るためのリスナー. */ @@ -50,7 +55,13 @@ public void onError(Exception e) { @Override public void onDrawn(EGLSurfaceBase eglSurfaceBase) { - // ignore. + if (mEGLSurfaceBase == eglSurfaceBase) { + VideoQuality videoQuality = getVideoQuality(); + if (videoQuality != null) { + eglSurfaceBase.setCropRect(videoQuality.getCropRect()); + } + onDrawnSurface(); + } } }; @@ -63,6 +74,17 @@ public SurfaceVideoEncoder(EGLSurfaceDrawingThread thread) { mInternalCreateSurfaceDrawingThread = false; } + /** + * 描画先の EGLSurfaceBase を取得します. + * + * エンコードが開始されていない場合には null を返却します。 + * + * @return 描画先の EGLSurfaceBase + */ + public EGLSurfaceBase getEGLSurfaceBase() { + return mEGLSurfaceBase; + } + // MediaEncoder @Override @@ -107,9 +129,11 @@ private void startDrawingThreadInternal() { } mSurfaceDrawingThread.setSize(quality.getVideoWidth(), quality.getVideoHeight()); - mSurfaceDrawingThread.addEGLSurfaceBase(mMediaCodecSurface); + mSurfaceDrawingThread.addEGLSurfaceBase(mMediaCodecSurface, quality.getCropRect()); mSurfaceDrawingThread.addOnDrawingEventListener(mOnDrawingEventListener); mSurfaceDrawingThread.start(); + + mEGLSurfaceBase = mSurfaceDrawingThread.findEGLSurfaceBaseByTag(mMediaCodecSurface); } /** @@ -123,6 +147,7 @@ private void stopDrawingThreadInternal() { if (mInternalCreateSurfaceDrawingThread) { mSurfaceDrawingThread = null; } + mEGLSurfaceBase = null; } } @@ -161,4 +186,10 @@ protected void onStartSurfaceDrawing() { */ protected void onStopSurfaceDrawing() { } + + /** + * Surface への描画を行ったことを通知します. + */ + protected void onDrawnSurface() { + } } diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/VideoEncoder.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/VideoEncoder.java index 6248796024..7e40b741e8 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/VideoEncoder.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/VideoEncoder.java @@ -19,16 +19,6 @@ public abstract class VideoEncoder extends MediaEncoder { private static final boolean DEBUG = BuildConfig.DEBUG; private static final String TAG = "VIDEO-ENCODER"; - /** - * キーフレームの同期フラグ. - */ - private boolean mSyncKeyFrame; - - /** - * 映像のビットレート変更要求フラグ. - */ - private boolean mRequestChangeBitRate; - // MediaEncoder @Override @@ -73,9 +63,21 @@ protected void release() { /** * キーフレームを要求します. + * + * @return 要求を受け付けた場合はtrue、それ以外はfalse */ - public void requestSyncKeyFrame() { - mSyncKeyFrame = true; + public boolean requestSyncKeyFrame() { + if (mMediaCodec != null) { + Bundle b = new Bundle(); + b.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0); + try { + mMediaCodec.setParameters(b); + return true; + } catch (Exception e) { + // ignore. + } + } + return false; } /** @@ -85,42 +87,21 @@ public void requestSyncKeyFrame() { * エンコード中にビットレートを変更したい場合に指定します。 * {@link VideoQuality#getBitRate()} で取得できるビットレートを再設定します。 *

+ * + * @return 要求を受け付けた場合はtrue、それ以外はfalse */ - public void requestBitRate() { - mRequestChangeBitRate = true; - } - - /** - * MediaCodec にキーフレームの作成を行います. - */ - private void syncKeyFrame() { - Bundle b = new Bundle(); - b.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0); - mMediaCodec.setParameters(b); - mSyncKeyFrame = false; - } - - /** - * MediaCodec にビットレートの変更を行います. - */ - private void changeBitRate() { - Bundle b = new Bundle(); - b.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, getVideoQuality().getBitRate()); - mMediaCodec.setParameters(b); - mRequestChangeBitRate = false; - } - - /** - * MediaCodec へのリクエスト処理を行います. - */ - protected void executeRequest() { - if (mSyncKeyFrame) { - syncKeyFrame(); - } - - if (mRequestChangeBitRate) { - changeBitRate(); + public boolean requestBitRate() { + if (mMediaCodec != null) { + Bundle b = new Bundle(); + b.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, getVideoQuality().getBitRate()); + try { + mMediaCodec.setParameters(b); + return true; + } catch (Exception e) { + // ignore. + } } + return false; } /** @@ -243,11 +224,20 @@ private MediaCodec createMediaCodec(int colorFormat, int w, int h) throws IOExce } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // エンコーダのレイテンシーを設定します。 - // 機種依存でサポートされていない場合には、この値は無視されます。 - format.setInteger(MediaFormat.KEY_LATENCY, 0); + Integer lowLatency = videoQuality.getLowLatency(); + if (lowLatency != null) { + if (lowLatency == 1 || lowLatency == 0) { + // エンコーダのレイテンシーを設定します。 + // 機種依存でサポートされていない場合には、この値は無視されます。 + format.setInteger(MediaFormat.KEY_LATENCY, lowLatency); + } + } } +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { +// format.setInteger(MediaFormat.KEY_MAX_FPS_TO_ENCODER, videoQuality.getFrameRate()); +// } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { int intraRefresh = videoQuality.getIntraRefresh(); if (intraRefresh != 0) { @@ -346,7 +336,7 @@ private List getMediaCodecInfo(String mimeType, int colorFormat) } - private void printCodecInfo(MediaCodecInfo codecInfo) { + private static void printCodecInfo(MediaCodecInfo codecInfo) { Log.i(TAG, "CODEC: " + codecInfo.getName()); String[] types = codecInfo.getSupportedTypes(); @@ -363,6 +353,21 @@ private void printCodecInfo(MediaCodecInfo codecInfo) { Log.i(TAG, " FORMAT: " + format); } + MediaCodecInfo.EncoderCapabilities encoderCapabilities = capabilities.getEncoderCapabilities(); + if (encoderCapabilities != null) { + Log.i(TAG, " ----"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + Log.i(TAG, " Quality range: " + + encoderCapabilities.getQualityRange().getLower() + + " - " + + encoderCapabilities.getQualityRange().getUpper()); + } + Log.i(TAG, " Complexity range: " + + encoderCapabilities.getComplexityRange().getLower() + + " - " + + encoderCapabilities.getComplexityRange().getUpper()); + } + MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); if (videoCapabilities != null) { Log.i(TAG, " ----"); @@ -390,7 +395,7 @@ private void printCodecInfo(MediaCodecInfo codecInfo) { } } - private void printCodecInfo() { + public static void printCodecInfo() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS); for (MediaCodecInfo codecInfo : list.getCodecInfos()) { diff --git a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/VideoQuality.java b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/VideoQuality.java index e73cf33375..79433740ba 100644 --- a/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/VideoQuality.java +++ b/dConnectSDK/dConnectLibStreaming/libmedia/src/main/java/org/deviceconnect/android/libmedia/streaming/video/VideoQuality.java @@ -1,5 +1,7 @@ package org.deviceconnect.android.libmedia.streaming.video; +import android.graphics.Rect; + public class VideoQuality { private static final int DEFAULT_FRAME_RATE = 30; private static final int DEFAULT_IFRAME_INTERVAL = 3; @@ -14,6 +16,7 @@ public class VideoQuality { private int mIntraRefresh = 0; private int mProfile = 0; private int mLevel = 0; + private Integer mLowLatency = 1; /** * ビットレートのモード. @@ -25,6 +28,17 @@ public class VideoQuality { */ private boolean mUseSoftwareEncoder; + /** + * 描画範囲. + * null の場合は範囲してい無し。 + */ + private Rect mCropRect; + + /** + * 画面の向き. + */ + private int mOrientation; + /** * コンストラクタ. * @param mimeType エンコードのマイムタイプ @@ -50,6 +64,9 @@ public void set(VideoQuality quality) { mUseSoftwareEncoder = quality.isUseSoftwareEncoder(); mProfile = quality.getProfile(); mLevel = quality.getLevel(); + if (quality.mCropRect != null) { + mCropRect = new Rect(quality.mCropRect); + } } /** @@ -263,6 +280,50 @@ public void setBitRateMode(BitRateMode bitRateMode) { mBitRateMode = bitRateMode; } + /** + * 描画範囲を取得します. + * + * 描画範囲指定されていない場合は null を返却します。 + * + * @return 描画範囲 + */ + public Rect getCropRect() { + return mCropRect; + } + + /** + * 描画範囲を設定します. + * + * null が指定された場合には、描画範囲を削除します。 + * + * @param cropRect 描画範囲 + */ + public void setCropRect(Rect cropRect) { + mCropRect = cropRect; + } + + /** + * Low Latency フラグを設定します. + * + * 1: 有効 + * 0: 無効 + * null: デフォルト設定 + * + * @param lowLatency Low Latency フラグ + */ + public void setLowLatency(Integer lowLatency) { + mLowLatency = lowLatency; + } + + /** + * Low Latency フラグを取得します. + * + * @return Low Latency フラグ + */ + public Integer getLowLatency() { + return mLowLatency; + } + /** * ビットレートモード. */ diff --git a/dConnectSDK/dConnectLibStreaming/libsrt/build.gradle b/dConnectSDK/dConnectLibStreaming/libsrt/build.gradle index c668d06bf6..7792a80219 100644 --- a/dConnectSDK/dConnectLibStreaming/libsrt/build.gradle +++ b/dConnectSDK/dConnectLibStreaming/libsrt/build.gradle @@ -6,7 +6,7 @@ if (githubPropertiesFile.exists()) { githubProperties.load(new FileInputStream(githubPropertiesFile)) } def getVersionName = { -> - return "1.2.3" // Replace with version Name + return "1.3.0" // Replace with version Name } def getArtificatId = { -> @@ -93,7 +93,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'org.deviceconnect:libmedia:1.2.2' + implementation 'org.deviceconnect:libmedia:1.3.0' // implementation project(':libmedia') testImplementation 'junit:junit:4.13.2'