Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5GMS Consumption reporting: Add support to Media Session Handler #38

Merged
merged 13 commits into from
Sep 26, 2023
Merged
2 changes: 1 addition & 1 deletion .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,37 @@ import android.content.Intent
import android.os.*
import android.util.Log
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.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
import com.fivegmag.a5gmsmediasessionhandler.network.ServiceAccessInformationApi
import com.fivegmag.a5gmsmediasessionhandler.network.ConsumptionReportingApi

import okhttp3.Headers
import okhttp3.ResponseBody
import okhttp3.OkHttpClient
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
Expand Down Expand Up @@ -74,6 +86,7 @@ class MediaSessionHandlerMessengerService() : Service() {
)

SessionHandlerMessageTypes.SET_M5_ENDPOINT -> setM5Endpoint(msg)
SessionHandlerMessageTypes.CONSUMPTION_REPORTING_MESSAGE -> reportConsumption(msg)
else -> super.handleMessage(msg)
}
}
Expand All @@ -87,15 +100,17 @@ class MediaSessionHandlerMessengerService() : Service() {
}

private fun handleStatusMessage(msg: Message) {

val sendingUid = msg.sendingUid;
val bundle: Bundle = msg.data as Bundle
val state: String = bundle.getString("playbackState", "")
Log.i(TAG, "[ConsumptionReporting] playbackState updated【$state】")
Toast.makeText(
applicationContext,
"Media Session Handler Service received state message: $state",
Toast.LENGTH_SHORT
).show()

clientsSessionData[msg.sendingUid]!!.playbackState = state
}


Expand Down Expand Up @@ -123,6 +138,10 @@ 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,
Expand All @@ -140,10 +159,10 @@ class MediaSessionHandlerMessengerService() : Service() {
msgResponse.data = responseBundle
responseMessenger.send(msgResponse)
}

}

override fun onFailure(call: Call<ServiceAccessInformation?>, t: Throwable) {
Log.i(TAG, "debug onFailure")
call.cancel()
}
})
Expand Down Expand Up @@ -253,7 +272,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 reporting timers.
* for the corresponding client id and reset all metric/comsumption reporting timers.
*
* @param clientId
*/
Expand All @@ -263,6 +282,8 @@ class MediaSessionHandlerMessengerService() : Service() {
clientsSessionData[clientId]?.serviceAccessInformation = null
clientsSessionData[clientId]?.serviceAccessInformationRequestTimer?.cancel()
clientsSessionData[clientId]?.serviceAccessInformationRequestTimer = null
clientsSessionData[clientId]?.consumptionReportingTimer?.cancel()
clientsSessionData[clientId]?.consumptionReportingTimer = null
clientsSessionData[clientId]?.serviceAccessInformationResponseHeaders = null
}
}
Expand Down Expand Up @@ -302,5 +323,177 @@ class MediaSessionHandlerMessengerService() : Service() {
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】")
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be re-evaluated for each consumption report while it should only be re-evaluated for each new streaming session

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand, it's purpose is to control the percentage of report(to reduce network footprint?)
I think may the description of this part of the spec is very unclear.@rjb1000

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right, @shilinding. When less than 100% of 5GMS Clients send consumption reports, the load on the 5GMS AF is reduced.

TS 26.512 clause 11.2.3.1 defines ServiceAccessInformaion.clientConsumptionReportingConfiguration.samplePercentage as:

The percentage of media streaming sessions that shall send consumption reports, expressed as a floating point value between 0.0 and 100.0.

When we discussed how to implement this aspect of metrics reporting, we realised that there is an implementation choice about exactly when to evaluate the random number generator that drive this behaviour:

  1. Generate the random number once for each streaming session. Reporting is then either enabled or disabled for that streaming session.
  2. Generate the random number each time you are thinking about sending a report.

This is left ambiguous in TS 26.512, and I think that's fine because it is a case where we can leave it to implementation choice and get a similar server load result across an entire population of 5GMS Clients.

For metrics reporting, we chose option 1. I think @dsilhavy's comment is suggesting that we choose option 1 for consumption reporting as well for consistency.

For option 1, there remains an open question in my mind about the implementation. If the random number determines that reporting is disabled for a session, does the 5GMS Client still need to collect samples, just in case the reporting configuration changes? Should our 5GMS Client implementation re-evaluate the random number if the client configuration changes? And, if it then needs to start reporting, does it need to report historic data? (Maybe that applies more to metrics reports than consumption reports, though.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably related to the issue created here: #32

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
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
}

private fun reportConsumption(msg: Message) {
val sendingUid = msg.sendingUid;
if (!isNeedReportConsumption(sendingUid))
{
Log.i(TAG, "[ConsumptionReporting] Not need ReportConsumption")
return
}

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}]"
)

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
}

val provisisioningSessionId: String = "2";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this value hardcoded?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't found the defintion of provisisioningSessionId in TS26.512, I didn't know how to generate it, so I hardcoded first.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the parameter is called provisioningSessionId, you have typos throughout your code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Thanks

val call: Call<ResponseBody>? = consumptionReportingApi.postConsumptionReporting(provisisioningSessionId, dataReporting?.reportingClientId);

call?.enqueue(object : retrofit2.Callback<ResponseBody> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
Log.i(TAG, "[ConsumptionReporting] resp from AF:" + response.body()?.string())
}

override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
Log.i(TAG, "[ConsumptionReporting] onFailure")
call.cancel()
}
})
}

private fun createRetrofitForConsumpReport(clientId: Int) {
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 retrofit = retrofitBuilder
.baseUrl(consumpReportUrl)
.build()

clientsSessionData[clientId]?.consumptionReportingApi = retrofit.create(ConsumptionReportingApi::class.java)
}
} catch (e: Exception) {
Log.e(TAG, "[ConsumptionReporting] onException of createRetrofitForConsumpReport")
}
}

fun startConsumptionReportTimer(clientId: Int) {
val timer = Timer()
clientsSessionData[clientId]?.serviceAccessInformationRequestTimer = timer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you overwrite the serviceAccessInformationRequestTimer? This timer is used to re-request the Service Access Information

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a typo and it should be consumptionReportingTimer, thanks for finding the bug. Corrected

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will address the remaining issues in the changes I am currently making. As this PR is already merged please do not add any more changes at this point as they will be lost (see also my comment at the bottom)


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consumption reports shall be send periodically:

Reports are sent at periodic intervals determined by the reporting interval attribute of the consumption reporting configuration specified in Table 4.2.3-2.

With this logic it seems like the next report is send only if a message from the MediaStreamHandler was dispatched and not at the predefined interval.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel that my understanding is not quite the same as yours, and I only found:

  • In 4.7.2.3: The Media Session Handler shall periodically check for updated Service Access Information
  • In 4.7.4: Upon determining the need to report ongoing 5GMS consumption at periodic intervals determined by the clientConsumptionReportingConfiguration.reportingInterval property.

As I understood, the report is send only if a message from the MediaStreamHandler was dispatched, and the conditions 4.7.4 occur including check the timer at the predefined interval. Am I misunderstood?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The consumption reports are sent as defined by reportingInterval. With each report the duration property is increased, see also here: 5G-MAG/rt-5gms-media-stream-handler#52

}
},
0,
(periodSec?.times(1000U))!!.toLong()
)
}

//// todo: support cacheControlHeader related flow
}

}
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package com.fivegmag.a5gmsmediasessionhandler.models

import android.os.Messenger
import com.fivegmag.a5gmscommonlibrary.helpers.PlayerStates
import com.fivegmag.a5gmscommonlibrary.models.ServiceAccessInformation
import com.fivegmag.a5gmsmediasessionhandler.network.ServiceAccessInformationApi
import com.fivegmag.a5gmsmediasessionhandler.network.ConsumptionReportingApi
import okhttp3.Headers
import java.util.Timer

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 serviceAccessInformationRequestTimer: Timer? = null,
var consumptionReportingTimer: Timer? = null,
var isConsumptionReportByTimer: Boolean = false,
var playbackState: String = PlayerStates.UNKNOWN
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,17 @@ https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view

package com.fivegmag.a5gmsmediasessionhandler.network

import com.fivegmag.a5gmscommonlibrary.models.ConsumptionReporting
import okhttp3.ResponseBody
import retrofit2.Call
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<ResponseBody>?
}
Loading