-
Notifications
You must be signed in to change notification settings - Fork 4
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
Changes from all commits
16332c8
5468ef3
0b2ee54
fcb38c6
306a7dd
8f1190f
94b055f
13898d3
7e40dc9
87d9c2b
00daeec
66c8df7
0b94262
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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 |
---|---|---|
|
@@ -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 | ||
|
@@ -74,6 +86,7 @@ class MediaSessionHandlerMessengerService() : Service() { | |
) | ||
|
||
SessionHandlerMessageTypes.SET_M5_ENDPOINT -> setM5Endpoint(msg) | ||
SessionHandlerMessageTypes.CONSUMPTION_REPORTING_MESSAGE -> reportConsumption(msg) | ||
else -> super.handleMessage(msg) | ||
} | ||
} | ||
|
@@ -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 | ||
} | ||
|
||
|
||
|
@@ -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, | ||
|
@@ -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() | ||
} | ||
}) | ||
|
@@ -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 | ||
*/ | ||
|
@@ -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 | ||
} | ||
} | ||
|
@@ -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 | ||
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"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this value hardcoded? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note the parameter is called There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The consumption reports are sent as defined by |
||
} | ||
}, | ||
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 | ||
) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
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:
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.)
There was a problem hiding this comment.
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