Skip to content

Commit

Permalink
Feature/metrics reporting (#56)
Browse files Browse the repository at this point in the history
* Working playback again after refactoring

* Distribute logic responsible for the different functionalities like metrics reporting and consumption reporting across different controllers

* Additional log out and refactoring

* Refactor structure to correspond to changes in MediaStreamHandler

* Add basic handling for ServiceAccessInformation changes in QoeMetricsReportingController

* Add sampling period

* Align implementation of reporting controllers by adjusting the message format

* Account for empty list of QoEMetrics reporting endpoints

* Never report QoE metrics if sample percentage is 0

* Refactor files to use "I" prefix for interfaces

* Incorporate changes from development

* Add support for recordingSessionId by genersting a mediaStreamingSessionIdentifier
  • Loading branch information
dsilhavy authored Apr 4, 2024
1 parent 2b4a6fd commit 9250eca
Show file tree
Hide file tree
Showing 20 changed files with 1,587 additions and 716 deletions.
7 changes: 5 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ android {
minSdk 29
targetSdk 33
versionCode 1
versionName "1.1.0"
versionName "1.2.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand All @@ -40,12 +40,15 @@ dependencies {
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation("com.google.android.gms:play-services-oss-licenses:17.0.1")
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

implementation 'org.greenrobot:eventbus:3.3.1'

// 5GMAG
implementation 'com.fivegmag:a5gmscommonlibrary:1.1.0'
implementation 'com.fivegmag:a5gmscommonlibrary:1.2.0'

// Retrofit
def retrofit_version = "2.9.0"
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
</activity>

<service
android:name=".MediaSessionHandlerMessengerService"
android:name=".service.MediaSessionHandlerMessengerService"
android:exported="true">
<intent-filter>
<action android:name="com.fivegmag.a5gmsmediasessionhandler.Service" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<entry key="defaultServiceAccessInformationTimerVal">9</entry>
<entry key="defaultServiceAccessInformationTimerInterval">10</entry>
</properties>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
package com.fivegmag.a5gmsmediasessionhandler.controller

import android.os.Bundle
import android.os.Message
import android.util.Log
import com.fivegmag.a5gmscommonlibrary.consumptionReporting.ConsumptionRequest
import com.fivegmag.a5gmscommonlibrary.helpers.ContentTypes
import com.fivegmag.a5gmscommonlibrary.helpers.SessionHandlerMessageTypes
import com.fivegmag.a5gmscommonlibrary.models.ClientConsumptionReportingConfiguration
import com.fivegmag.a5gmscommonlibrary.models.ServiceAccessInformation
import com.fivegmag.a5gmsmediasessionhandler.eventbus.ServiceAccessInformationUpdatedEvent
import com.fivegmag.a5gmsmediasessionhandler.models.ClientSessionData
import com.fivegmag.a5gmsmediasessionhandler.models.ConsumptionReportingSessionData
import com.fivegmag.a5gmsmediasessionhandler.network.IConsumptionReportingApi
import com.fivegmag.a5gmsmediasessionhandler.service.OutgoingMessageHandler
import okhttp3.MediaType
import okhttp3.RequestBody
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import retrofit2.Call
import retrofit2.Response
import retrofit2.Retrofit
import java.util.Timer
import java.util.TimerTask

class ConsumptionReportingController(
clientsSessionData: HashMap<Int, ClientSessionData>,
private val retrofitBuilder: Retrofit.Builder,
private val outgoingMessageHandler: OutgoingMessageHandler
) :
ReportingController(
clientsSessionData
) {

companion object {
const val TAG = "5GMS-ConsumptionReportingController"
}

fun handleConsumptionReportMessage(
msg: Message
) {
val bundle: Bundle = msg.data
val consumptionReport: String? =
bundle.getString("consumptionReport")
val clientId = msg.sendingUid

if (clientsSessionData[clientId]?.serviceAccessInformationSessionData?.serviceAccessInformation?.clientConsumptionReportingConfiguration == null
|| clientsSessionData[clientId]?.consumptionReportingSessionData?.api == null
) {
return
}

// Reset the timer once we received a report
stopReportingTimer(clientId)
startReportingTimer(clientId, null)

val clientSessionData = clientsSessionData[clientId]
val provisioningSessionId =
clientSessionData?.serviceAccessInformationSessionData?.serviceAccessInformation?.provisioningSessionId
val mediaType = MediaType.parse(ContentTypes.JSON)
val requestBody: RequestBody? =
consumptionReport?.let { RequestBody.create(mediaType, it) }

if (clientSessionData?.consumptionReportingSessionData?.api != null) {
val call: Call<Void>? =
clientSessionData.consumptionReportingSessionData.api!!.sendConsumptionReport(
provisioningSessionId,
requestBody
)

call?.enqueue(object : retrofit2.Callback<Void?> {
override fun onResponse(call: Call<Void?>, response: Response<Void?>) {
handleResponseCode(response, TAG)
}

override fun onFailure(call: Call<Void?>, t: Throwable) {
Log.d(TAG, "Error sending consumption report")
}
})
}
}

fun getConsumptionRequest(serviceAccessInformation: ServiceAccessInformation?): ConsumptionRequest {
val consumptionRequest = ConsumptionRequest()

if (serviceAccessInformation?.clientConsumptionReportingConfiguration != null) {
consumptionRequest.accessReporting =
serviceAccessInformation.clientConsumptionReportingConfiguration!!.accessReporting
consumptionRequest.locationReporting =
serviceAccessInformation.clientConsumptionReportingConfiguration!!.locationReporting
}

return consumptionRequest
}

private fun setReportingEndpoint(clientId: Int) {
clientsSessionData[clientId]?.consumptionReportingSessionData?.reportingSelectedServerAddress =
null
val clientConsumptionReportingConfiguration: ClientConsumptionReportingConfiguration? =
clientsSessionData[clientId]?.serviceAccessInformationSessionData?.serviceAccessInformation?.clientConsumptionReportingConfiguration

val serverAddress = clientConsumptionReportingConfiguration?.let {
selectReportingEndpoint(
it.serverAddresses
)
}

val retrofit = serverAddress?.let { retrofitBuilder.baseUrl(it).build() }
if (retrofit != null) {
clientsSessionData[clientId]?.consumptionReportingSessionData?.api =
retrofit.create(IConsumptionReportingApi::class.java)
clientsSessionData[clientId]?.consumptionReportingSessionData?.reportingSelectedServerAddress =
serverAddress
}
}

override fun initializeReportingTimer(
clientId: Int,
delay: Long?
) {
setReportingEndpoint(clientId)

// Do not start the consumption reporting timer if we dont have an endpoint
if (clientsSessionData[clientId]?.consumptionReportingSessionData?.api == null) {
return
}

val clientConsumptionReportingConfiguration: ClientConsumptionReportingConfiguration? =
clientsSessionData[clientId]?.serviceAccessInformationSessionData?.serviceAccessInformation?.clientConsumptionReportingConfiguration

if (clientConsumptionReportingConfiguration?.reportingInterval != null &&
clientConsumptionReportingConfiguration.reportingInterval!! > 0 &&
shouldReportAccordingToSamplePercentage(clientConsumptionReportingConfiguration.samplePercentage)
) {
startReportingTimer(clientId, delay)
}
}

private fun startReportingTimer(
clientId: Int,
delay: Long? = null
) {

// Endpoint not set
if (clientsSessionData[clientId]?.consumptionReportingSessionData?.api == null) {
setReportingEndpoint(clientId)
}

// Do nothing if the timer is already running
if (clientsSessionData[clientId]?.consumptionReportingSessionData?.reportingTimer != null) {
return
}

val clientConsumptionReportingConfiguration: ClientConsumptionReportingConfiguration =
clientsSessionData[clientId]?.serviceAccessInformationSessionData?.serviceAccessInformation?.clientConsumptionReportingConfiguration
?: return
val timer = Timer()
val reportingInterval =
clientConsumptionReportingConfiguration.reportingInterval!!.times(1000).toLong()
var finalDelay = delay
if (finalDelay == null) {
finalDelay = reportingInterval
}
timer.scheduleAtFixedRate(
object : TimerTask() {
override fun run() {
requestConsumptionReportFromClient(
clientId
)
}
},
finalDelay,
reportingInterval
)
clientsSessionData[clientId]?.consumptionReportingSessionData?.reportingTimer = timer
}

fun requestConsumptionReportFromClient(
clientId: Int
) {
val bundle = Bundle()
val locationReporting =
clientsSessionData[clientId]?.serviceAccessInformationSessionData?.serviceAccessInformation?.clientConsumptionReportingConfiguration?.locationReporting == true
val accessReporting =
clientsSessionData[clientId]?.serviceAccessInformationSessionData?.serviceAccessInformation?.clientConsumptionReportingConfiguration?.accessReporting == true
val consumptionRequest =
ConsumptionRequest(accessReporting, locationReporting)
bundle.putParcelable(
"consumptionRequest",
consumptionRequest
)
val messenger = clientsSessionData[clientId]?.messenger
if (messenger != null) {
outgoingMessageHandler.sendMessage(
SessionHandlerMessageTypes.GET_CONSUMPTION_REPORT,
bundle,
messenger
)
}
}

@Subscribe(threadMode = ThreadMode.MAIN)
override fun handleServiceAccessInformationChanges(
serviceAccessInformationUpdatedEvent: ServiceAccessInformationUpdatedEvent
) {
val previousClientConsumptionReportingConfiguration =
serviceAccessInformationUpdatedEvent.previousServiceAccessInformation?.clientConsumptionReportingConfiguration
val updatedClientConsumptionReportingConfiguration =
serviceAccessInformationUpdatedEvent.updatedServiceAccessInformation?.clientConsumptionReportingConfiguration

if (previousClientConsumptionReportingConfiguration == null || updatedClientConsumptionReportingConfiguration == null) {
return
}

val clientId = serviceAccessInformationUpdatedEvent.clientId

// location or access reporting has changed update the representation in the Media Stream Handler
if (previousClientConsumptionReportingConfiguration.accessReporting != updatedClientConsumptionReportingConfiguration.accessReporting || previousClientConsumptionReportingConfiguration.locationReporting != updatedClientConsumptionReportingConfiguration.locationReporting) {
updatePlaybackConsumptionReportingConfiguration(
clientId,
updatedClientConsumptionReportingConfiguration
)
}

// if the list of endpoints is empty we stop reporting. No need to check anything else
if (updatedClientConsumptionReportingConfiguration.serverAddresses.isEmpty()) {
stopReportingTimer(clientId)
clientsSessionData[clientId]?.consumptionReportingSessionData?.reportingSelectedServerAddress =
null
clientsSessionData[clientId]?.consumptionReportingSessionData?.api = null
return
}

// the currently used reporting endpoint has been removed from the list. Pick a new one
if (!updatedClientConsumptionReportingConfiguration.serverAddresses.contains(
clientsSessionData[clientId]?.consumptionReportingSessionData?.reportingSelectedServerAddress
)
) {
setReportingEndpoint(clientId)
}

// if sample percentage is set to 0 or no server addresses are available stop consumption reporting
if (updatedClientConsumptionReportingConfiguration.samplePercentage <= 0) {
stopReportingTimer(clientId)
}

// if sample percentage is set to 100 start consumption reporting
if (updatedClientConsumptionReportingConfiguration.samplePercentage >= 100) {
startReportingTimer(clientId)
}

// if sample percentage was previously zero and is now higher than zero evaluate again
if (updatedClientConsumptionReportingConfiguration.samplePercentage > 0 && previousClientConsumptionReportingConfiguration.samplePercentage <= 0 && shouldReportAccordingToSamplePercentage(
updatedClientConsumptionReportingConfiguration.samplePercentage
)
) {
startReportingTimer(clientId)
}

// updates of the reporting interval are handled automatically when stopping / starting the timer
}

private fun updatePlaybackConsumptionReportingConfiguration(
clientId: Int,
updatedClientConsumptionReportingConfiguration: ClientConsumptionReportingConfiguration
) {
val bundle = Bundle()
val locationReporting =
updatedClientConsumptionReportingConfiguration.locationReporting
val accessReporting =
updatedClientConsumptionReportingConfiguration.accessReporting
val consumptionRequest =
ConsumptionRequest(accessReporting, locationReporting)
bundle.putParcelable(
"consumptionRequest",
consumptionRequest
)
val messenger = clientsSessionData[clientId]?.messenger
if (messenger != null) {
outgoingMessageHandler.sendMessage(
SessionHandlerMessageTypes.UPDATE_PLAYBACK_CONSUMPTION_REPORTING_CONFIGURATION,
bundle,
messenger
)
}
}

override fun resetClientSession(clientId: Int) {
stopReportingTimer(clientId)
clientsSessionData[clientId]?.consumptionReportingSessionData =
ConsumptionReportingSessionData()
}

override fun reset() {
for (clientId in clientsSessionData.keys) {
resetClientSession(clientId)
}
}

override fun stopReportingTimer(
clientId: Int
) {
if (clientsSessionData[clientId]?.consumptionReportingSessionData?.reportingTimer != null) {
clientsSessionData[clientId]?.consumptionReportingSessionData?.reportingTimer?.cancel()
clientsSessionData[clientId]?.consumptionReportingSessionData?.reportingTimer = null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.fivegmag.a5gmsmediasessionhandler.controller

interface IController {

fun resetClientSession(clientId: Int)

fun reset()
}
Loading

0 comments on commit 9250eca

Please sign in to comment.