From 9980e8caa38865e0ca61cb40e80dc249d7d39284 Mon Sep 17 00:00:00 2001 From: Daniel Silhavy Date: Tue, 24 Oct 2023 14:46:45 +0200 Subject: [PATCH] Add required changes to derive consumption reports (#40) * Add required changes to derive consumption reports * Adjust request for consumption reporting * Fix Doc error --- .idea/compiler.xml | 2 +- app/build.gradle | 4 +- .../MediaSessionHandlerMessengerService.kt | 307 +++++++----------- .../models/ClientSessionModel.kt | 3 +- .../network/ConsumptionReportingApi.kt | 15 +- 5 files changed, 125 insertions(+), 206 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8..b589d56 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 38f37b0..3651962 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,7 +13,7 @@ android { minSdk 29 targetSdk 33 versionCode 1 - versionName "1.0.3" + versionName "1.1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -45,7 +45,7 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' // 5GMAG - implementation 'com.fivegmag:a5gmscommonlibrary:1.0.2' + implementation 'com.fivegmag:a5gmscommonlibrary:1.1.0' // Retrofit def retrofit_version = "2.9.0" diff --git a/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/MediaSessionHandlerMessengerService.kt b/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/MediaSessionHandlerMessengerService.kt index 37414ad..38dfa00 100644 --- a/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/MediaSessionHandlerMessengerService.kt +++ b/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/MediaSessionHandlerMessengerService.kt @@ -18,9 +18,9 @@ import android.widget.Toast import com.fivegmag.a5gmscommonlibrary.helpers.PlayerStates import com.fivegmag.a5gmscommonlibrary.helpers.SessionHandlerMessageTypes import com.fivegmag.a5gmscommonlibrary.helpers.Utils +import com.fivegmag.a5gmscommonlibrary.models.ClientConsumptionReportingConfiguration import com.fivegmag.a5gmscommonlibrary.models.EntryPoint import com.fivegmag.a5gmscommonlibrary.models.ServiceAccessInformation -import com.fivegmag.a5gmscommonlibrary.models.ConsumptionReporting import com.fivegmag.a5gmscommonlibrary.models.ServiceListEntry import com.fivegmag.a5gmsmediasessionhandler.models.ClientSessionModel import com.fivegmag.a5gmsmediasessionhandler.network.HeaderInterceptor @@ -28,24 +28,19 @@ import com.fivegmag.a5gmsmediasessionhandler.network.ServiceAccessInformationApi import com.fivegmag.a5gmsmediasessionhandler.network.ConsumptionReportingApi import okhttp3.Headers -import okhttp3.ResponseBody +import okhttp3.MediaType import okhttp3.OkHttpClient +import okhttp3.RequestBody import retrofit2.Call import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.Timer -import java.util.Date import java.util.TimerTask -import kotlin.math.abs -import kotlin.random.Random - const val TAG = "5GMS Media Session Handler" -const val SamplePercentageMax: Float = 100.0F; -const val EPSILON: Float = 0.0001F; /** * Create a bound service when you want to interact with the service from activities and other components in your application @@ -86,7 +81,7 @@ class MediaSessionHandlerMessengerService() : Service() { ) SessionHandlerMessageTypes.SET_M5_ENDPOINT -> setM5Endpoint(msg) - SessionHandlerMessageTypes.CONSUMPTION_REPORTING_MESSAGE -> reportConsumption(msg) + SessionHandlerMessageTypes.CONSUMPTION_REPORT -> handleConsumptionReportMessage(msg) else -> super.handleMessage(msg) } } @@ -110,7 +105,9 @@ class MediaSessionHandlerMessengerService() : Service() { Toast.LENGTH_SHORT ).show() - clientsSessionData[msg.sendingUid]!!.playbackState = state + when (state) { + PlayerStates.READY -> startConsumptionReportingTimer(sendingUid) + } } @@ -121,7 +118,6 @@ class MediaSessionHandlerMessengerService() : Service() { val responseMessenger: Messenger = msg.replyTo val sendingUid = msg.sendingUid; resetClientSession(sendingUid) - val provisioningSessionId: String = serviceListEntry!!.provisioningSessionId val call: Call? = clientsSessionData[msg.sendingUid]?.serviceAccessInformationApi?.fetchServiceAccessInformation( @@ -138,10 +134,6 @@ class MediaSessionHandlerMessengerService() : Service() { val resource = handleServiceAccessResponse(response, sendingUid, provisioningSessionId) - // create Retrofit for ConsumptionReporting, and start ConsumptionReport Timer - createRetrofitForConsumpReport(sendingUid) - startConsumptionReportTimer(sendingUid) - // Trigger the playback by providing all available entry points val msgResponse: Message = Message.obtain( null, @@ -239,8 +231,12 @@ class MediaSessionHandlerMessengerService() : Service() { val call: Call? = clientsSessionData[sendingUid]?.serviceAccessInformationApi?.fetchServiceAccessInformation( provisioningSessionId, - clientsSessionData[sendingUid]?.serviceAccessInformationResponseHeaders?.get("etag"), - clientsSessionData[sendingUid]?.serviceAccessInformationResponseHeaders?.get("last-modified") + clientsSessionData[sendingUid]?.serviceAccessInformationResponseHeaders?.get( + "etag" + ), + clientsSessionData[sendingUid]?.serviceAccessInformationResponseHeaders?.get( + "last-modified" + ) ) call?.enqueue(object : retrofit2.Callback { @@ -272,7 +268,7 @@ class MediaSessionHandlerMessengerService() : Service() { /** * Reset a client session once a new playback session is started. Remove the ServiceAccessInformation - * for the corresponding client id and reset all metric/comsumption reporting timers. + * for the corresponding client id and reset all metric/consumption reporting timers. * * @param clientId */ @@ -282,218 +278,143 @@ class MediaSessionHandlerMessengerService() : Service() { clientsSessionData[clientId]?.serviceAccessInformation = null clientsSessionData[clientId]?.serviceAccessInformationRequestTimer?.cancel() clientsSessionData[clientId]?.serviceAccessInformationRequestTimer = null + clientsSessionData[clientId]?.serviceAccessInformationResponseHeaders = null clientsSessionData[clientId]?.consumptionReportingTimer?.cancel() clientsSessionData[clientId]?.consumptionReportingTimer = null - clientsSessionData[clientId]?.serviceAccessInformationResponseHeaders = null } } + private fun startConsumptionReportingTimer(clientId: Int) { + val clientConsumptionReportingConfiguration: ClientConsumptionReportingConfiguration? = + clientsSessionData[clientId]?.serviceAccessInformation?.clientConsumptionReportingConfiguration - private fun setM5Endpoint(msg: Message) { - try { - val bundle: Bundle = msg.data - val m5BaseUrl: String? = bundle.getString("m5BaseUrl") - Log.i(TAG, "Setting M5 endpoint to $m5BaseUrl") - if (m5BaseUrl != null) { - val retrofit = retrofitBuilder - .baseUrl(m5BaseUrl) - .build() - clientsSessionData[msg.sendingUid]?.serviceAccessInformationApi = - retrofit.create(ServiceAccessInformationApi::class.java) - } - } catch (e: Exception) { + if (clientConsumptionReportingConfiguration?.reportingInterval != null && + clientConsumptionReportingConfiguration.reportingInterval!! > 0 && + shouldReportAccordingToSamplePercentage(clientConsumptionReportingConfiguration.samplePercentage) + ) { + val timer = Timer() + timer.scheduleAtFixedRate( + object : TimerTask() { + override fun run() { + requestConsumptionReportFromClient( + clientId + ) + } + }, + 0, + clientConsumptionReportingConfiguration.reportingInterval!!.times(1000).toLong() + ) + clientsSessionData[clientId]?.consumptionReportingTimer = timer + // Select one of the servers to report the metrics to + var serverAddress = + clientConsumptionReportingConfiguration.serverAddresses.random() + // Add a "/" in the end if not present + serverAddress = utils.addTrailingSlashIfNeeded(serverAddress) + val retrofit = retrofitBuilder.baseUrl(serverAddress).build() + clientsSessionData[clientId]?.consumptionReportingApi = + retrofit.create(ConsumptionReportingApi::class.java) } } - private fun triggerEvent() { - - } - } - /** - * When binding to the service, we return an interface to our messenger - * for sending messages to the service. To create a bound service, you must define the interface that specifies - * how a client can communicate with the service. This interface between the service and a client must be an implementation of - * IBinder and is what your service must return from the onBind() callback method. - */ - override fun onBind(intent: Intent): IBinder? { - Log.i("MediaSessionHandler-New", "Service bound new") - mMessenger = Messenger(IncomingHandler(this)) - return mMessenger.binder - } - - /** If the consumption reporting procedure is activated. Refer to TS26.512 Clause 4.7.4 - * When the clientConsumptionReportingConfiguration.samplePercentage value is 100, the Media Session Handler shall activate the consumption reporting procedure. - * If the samplePercentage is less than 100, the Media Session Handler shall generate a random number which is uniformly distributed in the range of 0 to 100, - * and the Media Session Handler shall activate the consumption report procedure when the generated random number is of a lower value than the samplePercentage value. - */ - private fun isConsumptionReportingActivated(clientId: Int): Boolean { - //if(currentServiceAccessInformation.isInitialized) - if(clientsSessionData[clientId]?.serviceAccessInformation == null) { - Log.i(TAG, "[ConsumptionReporting] IsConsumptionReportingActivated: ServiceAccessInformation is 【NULL】") + private fun shouldReportAccordingToSamplePercentage(samplePercentage: Float?): Boolean { + if (samplePercentage != null && samplePercentage <= 0) { return false } - // check if the samplePercentage in ServiceAccessInformation.clientConsumptionReportingConfiguration is valid - var samplePercentage: Float = clientsSessionData[clientId]?.serviceAccessInformation!!.clientConsumptionReportingConfiguration.samplePercentage; - if(samplePercentage > SamplePercentageMax || samplePercentage < 0) - { - Log.i(TAG, "[ConsumptionReporting] Invaild samplePercentage[$samplePercentage] in ServiceAccessInformation.clientConsumptionReportingConfiguration") - return false; - } - - // if samplePercentage is 100, MSH shall activate the consumption reporting procedure - if(abs(SamplePercentageMax - samplePercentage) < EPSILON) - { - Log.i(TAG, "[ConsumptionReporting] SamplePercentage==SamplePercentageMax, always report") - return true; - } - - // if the generated random number is of a lower value than the samplePercentage value - val randomFloat:Float = Random.nextFloat() - val randomInt:Int = Random.nextInt(0, SamplePercentageMax.toInt()) - val randomValue:Float = randomInt - 1 + randomFloat - Log.i(TAG, "[ConsumptionReporting] isConsumptionReportingActivated:myRandomValue[$randomValue],samplePercentage[$samplePercentage]") - - return randomValue < samplePercentage - } - - /** Refer to TS26.512 Clause 4.7.4 TS26.501 Clause 5.6.3 - If the consumption reporting procedure is activated, the Media Session Handler shall submit a consumption report to the 5GMSd AF - when any of the 5 conditions occur - */ - private fun isNeedReportConsumption(clientId: Int): Boolean { - // check IsConsumptionReportingActivated - if (!isConsumptionReportingActivated(clientId)) - { - Log.i(TAG, "[ConsumptionReporting] IsConsumptionReportingActivated is 【FALSE】") - return false - } - - // Condition 1&2: start/stop of consumption of a downlink streaming session - val state: String = clientsSessionData[clientId]!!.playbackState - if (PlayerStates.PLAYING == state || PlayerStates.ENDED == state ) - { - Log.i(TAG, "[ConsumptionReporting] IsConsumptionReportingActivated: report triggered by play status【${state}】 v2") - clientsSessionData[clientId]!!.playbackState = PlayerStates.UNKNOWN + if (samplePercentage == null || samplePercentage >= 100.0) { return true } - // Condition 3: check clientConsumptionReportingConfiguration.reportingInterval, timer trigger - if (clientsSessionData[clientId]!!.isConsumptionReportByTimer) - { - Log.i(TAG, "[ConsumptionReporting] IsConsumptionReportingActivated: report triggered by timer v2") - - clientsSessionData[clientId]!!.isConsumptionReportByTimer = false - return true - } - - // Condition 4&5:check clientConsumptionReportingConfiguration.locationReporting and clientConsumptionReportingConfiguration.accessReporting - if(clientsSessionData[clientId]?.serviceAccessInformation!!.clientConsumptionReportingConfiguration.locationReporting - || clientsSessionData[clientId]?.serviceAccessInformation!!.clientConsumptionReportingConfiguration.accessReporting) - { - Log.i(TAG, "[ConsumptionReporting] IsConsumptionReportingActivated: report triggered by locationReporting/accessReporting") - return true - } - - return false + return utils.generateRandomFloat() < samplePercentage } - private fun reportConsumption(msg: Message) { - val sendingUid = msg.sendingUid; - if (!isNeedReportConsumption(sendingUid)) - { - Log.i(TAG, "[ConsumptionReporting] Not need ReportConsumption") - return + private fun requestConsumptionReportFromClient( + clientId: Int + ) { + val msg: Message = Message.obtain( + null, + SessionHandlerMessageTypes.GET_CONSUMPTION_REPORT + ) + val bundle = Bundle() + val messenger = clientsSessionData[clientId]?.messenger + msg.data = bundle + msg.replyTo = mMessenger + try { + Log.i( + TAG, + "Request consumption report for client $clientId" + ) + messenger?.send(msg) + } catch (e: RemoteException) { + e.printStackTrace() } + } + private fun handleConsumptionReportMessage(msg: Message) { val bundle: Bundle = msg.data - bundle.classLoader = ConsumptionReporting::class.java.classLoader - val dataReporting: ConsumptionReporting? = bundle.getParcelable("consumptionData") - - Log.i(TAG, "[ConsumptionReporting] reportConsumption: ClientId[${dataReporting?.reportingClientId}] , " + - "Entry[${dataReporting?.mediaPlayerEntry}], " + - "startTime[${dataReporting?.consumptionReportingUnits?.get(0)?.startTime}]," + - "duration[${dataReporting?.consumptionReportingUnits?.get(0)?.duration}]," + - "ipAddr[${dataReporting?.consumptionReportingUnits?.get(0)?.mediaEndpointAddress?.ipv4Addr}/" + - "${dataReporting?.consumptionReportingUnits?.get(0)?.mediaEndpointAddress?.ipv6Addr}]" - ) + val consumptionReport: String? = + bundle.getString("consumptionReport") + val sendingUid = msg.sendingUid + val clientSessionModel = clientsSessionData[sendingUid] + val provisioningSessionId = + clientSessionModel?.serviceAccessInformation?.provisioningSessionId + val mediaType = MediaType.parse("application/json") + val requestBody: RequestBody? = consumptionReport?.let { RequestBody.create(mediaType, it) } + + if (clientSessionModel?.consumptionReportingApi != null) { + val call: Call? = + clientSessionModel.consumptionReportingApi!!.sendConsumptionReport( + provisioningSessionId, + requestBody + ) - Toast.makeText( - applicationContext, - "MSH recv Consumption-ID: ${dataReporting?.reportingClientId}", - Toast.LENGTH_LONG - ).show() - - // call m5 report consumption to AF - TS26.512 Clause 4.7.4 - val consumptionReportingApi: ConsumptionReportingApi? = clientsSessionData[sendingUid]?.consumptionReportingApi - if(consumptionReportingApi == null) - { - Log.i(TAG, "[ConsumptionReporting] consumptionReportingApi is 【NULL】") - return - } + call?.enqueue(object : retrofit2.Callback { + override fun onResponse(call: Call, response: Response) { + Log.d(TAG, "Successfully send consumption report") + } - val provisisioningSessionId: String = "2"; - val call: Call? = consumptionReportingApi.postConsumptionReporting(provisisioningSessionId, dataReporting?.reportingClientId); + override fun onFailure(call: Call, t: Throwable) { + Log.d(TAG, "Error sending consumption report") + } + }) + } + } - call?.enqueue(object : retrofit2.Callback { - override fun onResponse( - call: Call, - response: Response - ) { - Log.i(TAG, "[ConsumptionReporting] resp from AF:" + response.body()?.string()) - } - override fun onFailure(call: Call, t: Throwable) { - Log.i(TAG, "[ConsumptionReporting] onFailure") - call.cancel() - } - }) - } - - private fun createRetrofitForConsumpReport(clientId: Int) { + private fun setM5Endpoint(msg: Message) { try { - var consumpReportUrl: String = ""; - if(clientsSessionData[clientId]?.serviceAccessInformation!!.clientConsumptionReportingConfiguration.serverAddresses.isNotEmpty()) - { - consumpReportUrl = clientsSessionData[clientId]?.serviceAccessInformation!!.clientConsumptionReportingConfiguration.serverAddresses[0]; - } - Log.i(TAG, "[ConsumptionReporting] createRetrofitForConsumpReport URL: $consumpReportUrl.") - - if (consumpReportUrl != "") { + val bundle: Bundle = msg.data + val m5BaseUrl: String? = bundle.getString("m5BaseUrl") + Log.i(TAG, "Setting M5 endpoint to $m5BaseUrl") + if (m5BaseUrl != null) { val retrofit = retrofitBuilder - .baseUrl(consumpReportUrl) + .baseUrl(m5BaseUrl) .build() - - clientsSessionData[clientId]?.consumptionReportingApi = retrofit.create(ConsumptionReportingApi::class.java) + clientsSessionData[msg.sendingUid]?.serviceAccessInformationApi = + retrofit.create(ServiceAccessInformationApi::class.java) } } catch (e: Exception) { - Log.e(TAG, "[ConsumptionReporting] onException of createRetrofitForConsumpReport") } } - fun startConsumptionReportTimer(clientId: Int) { - val timer = Timer() - clientsSessionData[clientId]?.serviceAccessInformationRequestTimer = timer + private fun triggerEvent() { - if(clientsSessionData[clientId]?.serviceAccessInformation?.clientConsumptionReportingConfiguration?.reportingInterval != 0U) { - var periodSec: UInt? = - clientsSessionData[clientId]?.serviceAccessInformation?.clientConsumptionReportingConfiguration!!.reportingInterval - Log.i(TAG, "[ConsumptionReporting] startConsumptionReportTimer periodSec: $periodSec.") + } - timer.schedule( - object : TimerTask() { - override fun run() { - clientsSessionData[clientId]?.isConsumptionReportByTimer = true - } - }, - 0, - (periodSec?.times(1000U))!!.toLong() - ) - } - //// todo: support cacheControlHeader related flow + /** + * When binding to the service, we return an interface to our messenger + * for sending messages to the service. To create a bound service, you must define the interface that specifies + * how a client can communicate with the service. This interface between the service and a client must be an implementation of + * IBinder and is what your service must return from the onBind() callback method. + */ + override fun onBind(intent: Intent): IBinder? { + Log.i("MediaSessionHandler-New", "Service bound new") + mMessenger = Messenger(IncomingHandler(this)) + return mMessenger.binder } + } \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/models/ClientSessionModel.kt b/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/models/ClientSessionModel.kt index dbb2de5..295f529 100644 --- a/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/models/ClientSessionModel.kt +++ b/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/models/ClientSessionModel.kt @@ -12,10 +12,9 @@ data class ClientSessionModel( var messenger: Messenger?, var serviceAccessInformation: ServiceAccessInformation? = null, var serviceAccessInformationApi: ServiceAccessInformationApi? = null, - var consumptionReportingApi: ConsumptionReportingApi? = null, var serviceAccessInformationResponseHeaders: Headers? = null, var serviceAccessInformationRequestTimer: Timer? = null, + var consumptionReportingApi: ConsumptionReportingApi? = null, var consumptionReportingTimer: Timer? = null, - var isConsumptionReportByTimer: Boolean = false, var playbackState: String = PlayerStates.UNKNOWN ) \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/network/ConsumptionReportingApi.kt b/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/network/ConsumptionReportingApi.kt index 00d0105..a0194dd 100644 --- a/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/network/ConsumptionReportingApi.kt +++ b/app/src/main/java/com/fivegmag/a5gmsmediasessionhandler/network/ConsumptionReportingApi.kt @@ -9,17 +9,16 @@ https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view package com.fivegmag.a5gmsmediasessionhandler.network -import com.fivegmag.a5gmscommonlibrary.models.ConsumptionReporting -import okhttp3.ResponseBody +import okhttp3.RequestBody import retrofit2.Call +import retrofit2.http.Body import retrofit2.http.Path -import retrofit2.http.Field; -import retrofit2.http.FieldMap; -import retrofit2.http.FormUrlEncoded; import retrofit2.http.POST interface ConsumptionReportingApi { - @FormUrlEncoded - @POST("consumption-reporting/{provisisioningSessionId}") - fun postConsumptionReporting(@Path("provisisioningSessionId") provisisioningSessionId: String?, @Field("reportingClientId") reportingClientId: String?): Call? + @POST("consumption-reporting/{provisioningSessionId}") + fun sendConsumptionReport( + @Path("provisioningSessionId") provisioningSessionId: String?, + @Body requestBody: RequestBody? + ): Call? } \ No newline at end of file