diff --git a/.idea/misc.xml b/.idea/misc.xml index 9f71c83..773fe0f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/build.gradle b/app/build.gradle index 8df3dc6..0840a9c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,7 +12,7 @@ android { minSdk 29 targetSdk 32 versionCode 1 - versionName "1.1.0" + versionName "1.2.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -37,7 +37,7 @@ publishing { release(MavenPublication) { groupId = 'com.fivegmag' artifactId = 'a5gmsmediastreamhandler' - version = '1.1.0' + version = '1.2.0' afterEvaluate { from components.release } @@ -61,13 +61,21 @@ dependencies { implementation 'androidx.media3:media3-exoplayer-dash:1.0.2' implementation 'androidx.media3:media3-exoplayer-hls:1.0.2' implementation 'androidx.media3:media3-ui:1.0.2' + implementation 'org.greenrobot:eventbus:3.3.1' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1' implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.5.1' - implementation 'com.fivegmag:a5gmscommonlibrary:1.1.0' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.1' - implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.1' + + implementation 'javax.xml.stream:stax-api:1.0-2' + implementation 'com.fasterxml.jackson.core:jackson-core:2.15.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.15.0' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.15.0' + + // 5G-MAG + implementation 'com.fivegmag:a5gmscommonlibrary:1.2.0' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.4' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/MediaSessionHandlerAdapter.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/MediaSessionHandlerAdapter.kt index ca98cf8..5dc37a7 100644 --- a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/MediaSessionHandlerAdapter.kt +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/MediaSessionHandlerAdapter.kt @@ -9,302 +9,197 @@ https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view package com.fivegmag.a5gmsmediastreamhandler +import android.Manifest import android.annotation.SuppressLint -import android.content.ComponentName import android.content.Context -import android.content.Intent -import android.content.ServiceConnection +import android.content.pm.PackageManager import android.os.* -import android.util.Log -import android.widget.Toast +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat import androidx.media3.common.util.UnstableApi -import com.fivegmag.a5gmscommonlibrary.eventbus.CellInfoUpdatedEvent -import com.fivegmag.a5gmscommonlibrary.eventbus.PlaybackStateChangedEvent -import com.fivegmag.a5gmscommonlibrary.helpers.ContentTypes -import com.fivegmag.a5gmscommonlibrary.helpers.PlayerStates -import com.fivegmag.a5gmscommonlibrary.helpers.SessionHandlerMessageTypes +import com.fivegmag.a5gmscommonlibrary.helpers.Utils import com.fivegmag.a5gmscommonlibrary.models.* -import com.fivegmag.a5gmsmediastreamhandler.consumptionReporting.ConsumptionReportingController -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import com.fivegmag.a5gmsmediastreamhandler.controller.ConsumptionReportingController +import com.fivegmag.a5gmsmediastreamhandler.controller.QoEMetricsReportingController +import com.fivegmag.a5gmsmediastreamhandler.controller.SessionController +import com.fivegmag.a5gmsmediastreamhandler.player.exoplayer.ExoPlayerAdapter +import com.fivegmag.a5gmsmediastreamhandler.service.IncomingMessageHandler +import com.fivegmag.a5gmsmediastreamhandler.service.MessengerService +import com.fivegmag.a5gmsmediastreamhandler.service.OutgoingMessageHandler import java.util.* +@UnstableApi class MediaSessionHandlerAdapter() { - private val TAG: String = "MediaSessionHandlerAdapter" - private var mService: Messenger? = null - private var bound: Boolean = false - private lateinit var exoPlayerAdapter: ExoPlayerAdapter + companion object { + const val TAG = "5GMS-MediaSessionHandlerAdapter" + } + + private lateinit var context: Context + private lateinit var messengerService: MessengerService private lateinit var consumptionReportingController: ConsumptionReportingController - private lateinit var serviceConnectedCallbackFunction: () -> Unit + private lateinit var qoeMetricsReportingController: QoEMetricsReportingController + private lateinit var sessionController: SessionController + private lateinit var outgoingMessageHandler: OutgoingMessageHandler + private lateinit var incomingMessageHandler: IncomingMessageHandler + private val utils = Utils() + private val exoPlayerAdapter = ExoPlayerAdapter() /** - * Handler of incoming messages from clients. + * API endpoint to initialize Media Session handling. Connects to the Media Session Handler and calls the provided callback function afterwards + * + * @param ctxt + * @param epa + * @param onConnectionToMediaSessionHandlerEstablished */ - @UnstableApi - inner class IncomingHandler() : Handler() { - override fun handleMessage(msg: Message) { - when (msg.what) { - SessionHandlerMessageTypes.SESSION_HANDLER_TRIGGERS_PLAYBACK -> handleSessionHandlerTriggersPlaybackMessage( - msg - ) - - SessionHandlerMessageTypes.GET_CONSUMPTION_REPORT -> handleGetConsumptionReportMessage( - msg - ) - SessionHandlerMessageTypes.UPDATE_PLAYBACK_CONSUMPTION_REPORTING_CONFIGURATION -> handleUpdatePlaybackConsumptionReportingConfiguration( - msg - ) - - else -> super.handleMessage(msg) - } - } + fun initialize( + context: Context, + onConnectionToMediaSessionHandlerEstablished: () -> (Unit) + ) { + this.context = context - private fun handleSessionHandlerTriggersPlaybackMessage(msg: Message) { - val bundle: Bundle = msg.data - bundle.classLoader = PlaybackRequest::class.java.classLoader - val playbackRequest: PlaybackRequest? = bundle.getParcelable("playbackRequest") + outgoingMessageHandler = OutgoingMessageHandler() + incomingMessageHandler = IncomingMessageHandler() - if (playbackRequest != null && playbackRequest.entryPoints.size > 0) { - val dashEntryPoints: List = - playbackRequest.entryPoints.filter { entryPoint -> entryPoint.contentType == ContentTypes.DASH } + val reportingClientId = generateReportingClientId() - if (dashEntryPoints.isNotEmpty()) { - val mpdUrl = dashEntryPoints[0].locator - exoPlayerAdapter.handleSourceChange() - consumptionReportingController.setCurrentConsumptionReportingConfiguration( - playbackRequest.playbackConsumptionReportingConfiguration - ) - exoPlayerAdapter.attach(mpdUrl, ContentTypes.DASH) - exoPlayerAdapter.preload() - exoPlayerAdapter.play() - } - } - } + consumptionReportingController = + ConsumptionReportingController(exoPlayerAdapter, outgoingMessageHandler) + consumptionReportingController.reportingClientId = reportingClientId + consumptionReportingController.initialize() - private fun handleUpdatePlaybackConsumptionReportingConfiguration(msg: Message) { - val bundle: Bundle = msg.data - bundle.classLoader = PlaybackConsumptionReportingConfiguration::class.java.classLoader - val playbackConsumptionReportingConfiguration: PlaybackConsumptionReportingConfiguration? = - bundle.getParcelable("playbackConsumptionReportingConfiguration") + qoeMetricsReportingController = + QoEMetricsReportingController(exoPlayerAdapter, outgoingMessageHandler) + qoeMetricsReportingController.reportingClientId = reportingClientId + qoeMetricsReportingController.initialize() - if (playbackConsumptionReportingConfiguration != null) { - consumptionReportingController.setCurrentConsumptionReportingConfiguration( - playbackConsumptionReportingConfiguration - ) - } - } + sessionController = SessionController( + context, + exoPlayerAdapter, + outgoingMessageHandler + ) + sessionController.initialize() - private fun handleGetConsumptionReportMessage(msg: Message) { - val bundle: Bundle = msg.data - bundle.classLoader = PlaybackConsumptionReportingConfiguration::class.java.classLoader - val playbackConsumptionReportingConfiguration: PlaybackConsumptionReportingConfiguration? = - bundle.getParcelable("playbackConsumptionReportingConfiguration") + incomingMessageHandler.initialize( + consumptionReportingController, + qoeMetricsReportingController, + sessionController + ) - if (playbackConsumptionReportingConfiguration != null) { - consumptionReportingController.setCurrentConsumptionReportingConfiguration( - playbackConsumptionReportingConfiguration - ) - } - sendConsumptionReport() - consumptionReportingController.cleanConsumptionReportingList() - } + messengerService = MessengerService(this.context) + messengerService.initialize(incomingMessageHandler, outgoingMessageHandler) + messengerService.bind(onConnectionToMediaSessionHandlerEstablished) } /** - * Target we publish for clients to send messages to IncomingHandler. + * The GPSI is either a mobile subscriber ISDN number (MSISDN) or an external identifier + * */ - @SuppressLint("UnsafeOptInUsageError") - private val mMessenger: Messenger = Messenger(IncomingHandler()) + @SuppressLint("Range") + fun generateReportingClientId(): String { + val strGpsi: String + var strMsisdn = "" + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { + strMsisdn = getMsisdn() + } + strGpsi = if (strMsisdn != "") { + strMsisdn + } else { + utils.generateUUID() + } + + return strGpsi + } /** - * Class for interacting with the main interface of the service. + * MSISDN = CC + NDC + SN + * */ - private val mConnection = object : ServiceConnection { - - override fun onServiceConnected(className: ComponentName, service: IBinder) { - // This is called when the connection with the service has been - // established, giving us the object we can use to - // interact with the service. We are communicating with the - // service using a Messenger, so here we get a client-side - // representation of that from the raw IBinder object. - mService = Messenger(service) - try { - val msg: Message = Message.obtain( - null, - SessionHandlerMessageTypes.REGISTER_CLIENT - ) - msg.replyTo = mMessenger - mService!!.send(msg) - bound = true - serviceConnectedCallbackFunction() - } catch (e: RemoteException) { - // In this case the service has crashed before we could even - // do anything with it; we can count on soon being - // disconnected (and then reconnected if it can be restarted) - // so there is no need to do anything here. + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun getMsisdn(): String { + var strMsisdn = "" + + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.READ_PHONE_NUMBERS + ) == PackageManager.PERMISSION_GRANTED + ) { + val subscriptionManager: SubscriptionManager = + context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager + + val subscriptionInfoList: List = + subscriptionManager.activeSubscriptionInfoList + for (subscriptionInfo in subscriptionInfoList) { + strMsisdn = + subscriptionManager.getPhoneNumber(getActiveSIMIdx(subscriptionInfoList)) } - } - override fun onServiceDisconnected(className: ComponentName) { - // This is called when the connection with the service has been - // unexpectedly disconnected -- that is, its process crashed. - mService = null - bound = false - } + return strMsisdn } - @UnstableApi - fun initialize( - context: Context, - epa: ExoPlayerAdapter, - onConnectionToMediaSessionHandlerEstablished: () -> (Unit) - ) { - exoPlayerAdapter = epa - consumptionReportingController = ConsumptionReportingController(context) - consumptionReportingController.initialize() - EventBus.getDefault().register(this) - - try { - val intent = Intent() - intent.component = ComponentName( - "com.fivegmag.a5gmsmediasessionhandler", - "com.fivegmag.a5gmsmediasessionhandler.MediaSessionHandlerMessengerService" - ) - if (context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE)) { - Log.i(TAG, "Binding to MediaSessionHandler service returned true") - Toast.makeText( - context, - "Successfully bound to Media Session Handler", - Toast.LENGTH_SHORT - ).show() - } else { - Log.d(TAG, "Binding to MediaSessionHandler service returned false") + /** + * In case of multi SIM cards, get the the index of SIM which is used for the traffic + * If none of them match, use the first as default + * + */ + private fun getActiveSIMIdx(subscriptionInfoList: List): Int { + val telephonyManager = + context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + val simOPName: String = telephonyManager.simOperatorName + + var subscriptionIdx = 1 + for (subscriptionInfo in subscriptionInfoList) { + val subscriptionId = subscriptionInfo.subscriptionId + val subscriptionName: String = subscriptionInfo.carrierName as String + + if (subscriptionName == simOPName) { + subscriptionIdx = subscriptionId } - serviceConnectedCallbackFunction = onConnectionToMediaSessionHandlerEstablished - } catch (e: SecurityException) { - Log.e( - TAG, - "Can't bind to MediaSessionHandler, check permission in Manifest" - ) - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @UnstableApi - fun onPlaybackStateChangedEvent(event: PlaybackStateChangedEvent) { - if (event.playbackState == PlayerStates.ENDED) { - sendConsumptionReport() } - updatePlaybackState(event.playbackState) + return subscriptionIdx } - @Subscribe(threadMode = ThreadMode.MAIN) - @UnstableApi - fun onCellInfoUpdatedEvent(event: CellInfoUpdatedEvent) { - val playbackConsumptionReportingConfiguration = - consumptionReportingController.getPlaybackConsumptionReportingConfiguration() - if (playbackConsumptionReportingConfiguration != null && playbackConsumptionReportingConfiguration.locationReporting == true) { - sendConsumptionReport() - } - } - - fun reset(context: Context) { - if (bound) { - context.unbindService(mConnection) - bound = false - } + /** + * API endpoint to close the connection to the MediaSessionHandler + * + */ + fun reset() { + messengerService.reset() } + /** + * API endpoint for application to set the M5 endpoint in the MediaSessionHandler. + * An IPC call is send by this function to the MediaSessionHandler to update the M5 URL. + * + * @param m5BaseUrl + */ fun setM5Endpoint(m5BaseUrl: String) { - val msg: Message = Message.obtain( - null, - SessionHandlerMessageTypes.SET_M5_ENDPOINT - ) - val bundle = Bundle() - bundle.putString("m5BaseUrl", m5BaseUrl) - msg.data = bundle - msg.replyTo = mMessenger - try { - mService?.send(msg) - } catch (e: RemoteException) { - e.printStackTrace() - } - } - - fun updatePlaybackState(state: String) { - if (!bound) return - val msg: Message = Message.obtain(null, SessionHandlerMessageTypes.STATUS_MESSAGE) - val bundle = Bundle() - bundle.putString("playbackState", state) - msg.data = bundle - try { - mService?.send(msg) - } catch (e: RemoteException) { - e.printStackTrace() - } - } - - fun reportMetrics() { - if (!bound) return - // Create and send a message to the service, using a supported 'what' value - val msg: Message = Message.obtain(null, SessionHandlerMessageTypes.METRIC_REPORTING_MESSAGE) - val bundle = Bundle() - bundle.putString("metricData", "") - msg.data = bundle - try { - mService?.send(msg) - } catch (e: RemoteException) { - e.printStackTrace() - } + messengerService.setM5Endpoint(m5BaseUrl) } - + /** + * API endpoint to send a ServiceListEntry to the MediaSessionHandler to trigger the playback process. + * + * @param serviceListEntry + */ fun initializePlaybackByServiceListEntry(serviceListEntry: ServiceListEntry) { - if (!bound) return - // Create and send a message to the service, using a supported 'what' value - val msg: Message = Message.obtain( - null, - SessionHandlerMessageTypes.START_PLAYBACK_BY_SERVICE_LIST_ENTRY_MESSAGE - ) - val bundle = Bundle() - bundle.putParcelable("serviceListEntry", serviceListEntry) - msg.data = bundle - msg.replyTo = mMessenger - try { - mService?.send(msg) - } catch (e: RemoteException) { - e.printStackTrace() + if (exoPlayerAdapter.hasActiveMediaItem()) { + consumptionReportingController.triggerConsumptionReport() + qoeMetricsReportingController.triggerQoeMetricsReports() } + messengerService.initializePlaybackByServiceListEntry(serviceListEntry) } - @UnstableApi - fun sendConsumptionReport() { - val mediaPlayerEntry = exoPlayerAdapter.getCurrentManifestUri() - val consumptionReport = - consumptionReportingController.getConsumptionReport(mediaPlayerEntry) - val msg: Message = Message.obtain( - null, - SessionHandlerMessageTypes.CONSUMPTION_REPORT - ) - - val bundle = Bundle() - bundle.putString("consumptionReport", consumptionReport) - msg.data = bundle - msg.replyTo = mMessenger - try { - mService?.send(msg) - } catch (e: RemoteException) { - e.printStackTrace() - } - } - - @UnstableApi - fun resetState() { - consumptionReportingController.resetState() + /** + * API endpoint to get the instance of ExoPlayerAdapter + * + */ + fun getExoPlayerAdapter(): ExoPlayerAdapter { + return exoPlayerAdapter } - } \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/consumptionReporting/ConsumptionReportingController.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/consumptionReporting/ConsumptionReportingController.kt deleted file mode 100644 index b2c7786..0000000 --- a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/consumptionReporting/ConsumptionReportingController.kt +++ /dev/null @@ -1,406 +0,0 @@ -package com.fivegmag.a5gmsmediastreamhandler.consumptionReporting - -import android.Manifest -import android.annotation.SuppressLint -import android.content.Context -import android.content.pm.PackageManager -import android.os.Build -import android.telephony.* -import androidx.annotation.RequiresApi -import androidx.core.app.ActivityCompat -import androidx.media3.common.util.Log -import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.source.MediaLoadData -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor -import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.databind.ser.PropertyFilter -import com.fasterxml.jackson.databind.ser.PropertyWriter -import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fivegmag.a5gmscommonlibrary.consumptionReporting.ConsumptionReport -import com.fivegmag.a5gmscommonlibrary.consumptionReporting.ConsumptionReportingUnit -import com.fivegmag.a5gmscommonlibrary.eventbus.CellInfoUpdatedEvent -import com.fivegmag.a5gmscommonlibrary.eventbus.DownstreamFormatChangedEvent -import com.fivegmag.a5gmscommonlibrary.eventbus.LoadStartedEvent -import com.fivegmag.a5gmscommonlibrary.helpers.Utils -import com.fivegmag.a5gmscommonlibrary.models.CellIdentifierType -import com.fivegmag.a5gmscommonlibrary.models.EndpointAddress -import com.fivegmag.a5gmscommonlibrary.models.PlaybackConsumptionReportingConfiguration -import com.fivegmag.a5gmscommonlibrary.models.TypedLocation -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import java.lang.Exception -import java.util.Date - -@UnstableApi -class ConsumptionReportingController( - private val context: Context -) { - private val TAG = "ConsumptionReportingController" - private val utils: Utils = Utils() - private val reportingClientId = generateReportingClientId() - private val consumptionReportingUnitList: ArrayList = ArrayList() - private var playbackConsumptionReportingConfiguration: PlaybackConsumptionReportingConfiguration? = - null - private var activeLocations: ArrayList = ArrayList() - private var serverEndpointAddressesPerMediaType = mutableMapOf() - private val cellInfoCallback = object : TelephonyManager.CellInfoCallback() { - override fun onCellInfo(cellInfoList: MutableList) { - // Process the received cell info - activeLocations.clear() - for (cellInfo in cellInfoList) { - if (cellInfo.isRegistered && (cellInfo.cellConnectionStatus == CellInfo.CONNECTION_PRIMARY_SERVING || cellInfo.cellConnectionStatus == CellInfo.CONNECTION_SECONDARY_SERVING)) { - val location = createTypedLocationByCellInfo(cellInfo) - if (location != null) { - activeLocations.add(location) - } - } - } - - EventBus.getDefault().post(CellInfoUpdatedEvent(cellInfoList)) - } - } - - /** - * MSISDN = CC + NDC + SN - * - */ - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - private fun getMsisdn(): String { - var strMsisdn = "" - - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.READ_PHONE_NUMBERS - ) == PackageManager.PERMISSION_GRANTED - ) { - val subscriptionManager: SubscriptionManager = - context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager - - val subscriptionInfoList: List = - subscriptionManager.activeSubscriptionInfoList - for (subscriptionInfo in subscriptionInfoList) { - strMsisdn = - subscriptionManager.getPhoneNumber(getActiveSIMIdx(subscriptionInfoList)) - } - } - - return strMsisdn - } - - /** - * The GPSI is either a mobile subscriber ISDN number (MSISDN) or an external identifier - * - */ - @SuppressLint("Range") - private fun generateReportingClientId(): String { - val strGpsi: String - var strMsisdn = "" - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { - strMsisdn = getMsisdn() - } - strGpsi = if (strMsisdn != "") { - strMsisdn - } else { - utils.generateUUID() - } - - Log.d(TAG, "ConsumptionReporting: GPSI = $strGpsi") - return strGpsi - } - - /** - * In case of multi SIM cards, get the the index of SIM which is used for the traffic - * If none of them match, use the first as default - * - */ - private fun getActiveSIMIdx(subscriptionInfoList: List): Int { - val telephonyManager = - context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - val simOPName: String = telephonyManager.simOperatorName - - var subscriptionIdx = 1 - for (subscriptionInfo in subscriptionInfoList) { - val subscriptionId = subscriptionInfo.subscriptionId - val subscriptionName: String = subscriptionInfo.carrierName as String - - if (subscriptionName == simOPName) { - subscriptionIdx = subscriptionId - } - } - - return subscriptionIdx - } - - fun getPlaybackConsumptionReportingConfiguration(): PlaybackConsumptionReportingConfiguration? { - return playbackConsumptionReportingConfiguration - } - - private fun createTypedLocationByCellInfo(cellInfo: CellInfo): TypedLocation? { - try { - val typedLocation: TypedLocation? - when (cellInfo) { - // CGI = MCC + MNC + LAC + CI - is CellInfoGsm -> { - val cellIdentity = cellInfo.cellIdentity as CellIdentityGsm - val mcc = cellIdentity.mccString - val mnc = cellIdentity.mncString - val lac = cellIdentity.lac - val ci = cellIdentity.cid - typedLocation = TypedLocation(CellIdentifierType.CGI, "$mcc$mnc$lac$ci") - } - // ECGI = MCC + MNC + ECI - is CellInfoLte -> { - val cellIdentity = cellInfo.cellIdentity as CellIdentityLte - val mcc = cellIdentity.mccString - val mnc = cellIdentity.mncString - val eci = cellIdentity.ci - typedLocation = TypedLocation(CellIdentifierType.ECGI, "$mcc$mnc$eci") - } - // NCGI = MCC + MNC + NCI - is CellInfoNr -> { - val cellIdentity = cellInfo.cellIdentity as CellIdentityNr - val mcc = cellIdentity.mccString - val mnc = cellIdentity.mncString - val nci = cellIdentity.nci - typedLocation = TypedLocation(CellIdentifierType.NCGI, "$mcc$mnc$nci") - } - - else -> { - return null - } - } - - return typedLocation - } catch (e: Exception) { - return null - } - } - - fun initialize() { - EventBus.getDefault().register(this) - requestCellInfoUpdates() - } - - fun resetState() { - playbackConsumptionReportingConfiguration = null - serverEndpointAddressesPerMediaType.clear() - consumptionReportingUnitList.clear() - } - - fun setCurrentConsumptionReportingConfiguration(consumptionReportingConfiguration: PlaybackConsumptionReportingConfiguration) { - playbackConsumptionReportingConfiguration = consumptionReportingConfiguration - } - - /** - * Callback function that is triggered via the event bus - * - * @param {DownstreamFormatChangedEvent} event - */ - @RequiresApi(Build.VERSION_CODES.R) - @Subscribe(threadMode = ThreadMode.MAIN) - fun onDownstreamFormatChangedEvent(event: DownstreamFormatChangedEvent) { - addConsumptionReportingUnit(event.mediaLoadData) - } - - @SuppressLint("Range") - @Subscribe(threadMode = ThreadMode.MAIN) - fun onLoadStartedEvent(event: LoadStartedEvent) { - try { - val mimeType = event.mediaLoadData.trackFormat!!.containerMimeType - if (mimeType != null) { - val requestUrl = event.loadEventInfo.dataSpec.uri.toString() - val endpointAddress = utils.getEndpointAddressByRequestUrl(requestUrl) - if (endpointAddress != null) { - serverEndpointAddressesPerMediaType[mimeType] = endpointAddress - } - } - } catch (e: Exception) { - Log.d(TAG, "Error while creating server endpoint address: $e") - } - } - - - @SuppressLint("Range") - @RequiresApi(Build.VERSION_CODES.R) - private fun addConsumptionReportingUnit(mediaLoadData: MediaLoadData) { - val startTime = utils.formatDateToOpenAPIFormat(Date()) - val mediaConsumed = mediaLoadData.trackFormat?.id - val mimeType = mediaLoadData.trackFormat!!.containerMimeType - val duration = 0 - - // If we have a previous entry in the list of consumption reporting units with the same media type then we can calculate the duration now - val existingEntry = consumptionReportingUnitList.find { it.mimeType == mimeType } - if (existingEntry != null) { - existingEntry.duration = - utils.calculateTimestampDifferenceInSeconds(existingEntry.startTime, startTime) - .toInt() - existingEntry.finished = true - } - - Log.d(TAG, playbackConsumptionReportingConfiguration.toString()) - - // Add the new entry - val consumptionReportingUnit = - mediaConsumed?.let { - ConsumptionReportingUnit( - it, - null, - null, - startTime, - duration, - null - ) - } - if (consumptionReportingUnit != null) { - consumptionReportingUnit.mimeType = mimeType - - // We add locations and clientEndpointAddress and filter the attributes later in case the corresponding configuration options are set to false - // If we do not add the information here we can not include it later in case the SAI configuration changes - consumptionReportingUnit.locations = activeLocations - consumptionReportingUnit.serverEndpointAddress = getServerEndpointAddress(mimeType) - consumptionReportingUnit.clientEndpointAddress = - getClientEndpointAddress(consumptionReportingUnit.serverEndpointAddress) - consumptionReportingUnitList.add(consumptionReportingUnit) - } - } - - private fun getClientEndpointAddress(serverEndpointAddress: EndpointAddress?): EndpointAddress { - val ipv4Address = utils.getIpAddress(4) - val ipv6Address = utils.getIpAddress(6) - var portNumber = 80 - if (serverEndpointAddress != null) { - portNumber = serverEndpointAddress.portNumber - } - - return EndpointAddress(null, ipv4Address, ipv6Address, portNumber) - } - - private fun getServerEndpointAddress(mimeType: String?): EndpointAddress? { - if (mimeType == null) { - return null - } - - return serverEndpointAddressesPerMediaType[mimeType] ?: return null - } - - fun getConsumptionReport(mediaPlayerEntry: String): String { - // We need to add the duration of the consumption reporting units that are not yet finished - for (consumptionReportingUnit in consumptionReportingUnitList) { - if (!consumptionReportingUnit.finished) { - val currentTime = utils.formatDateToOpenAPIFormat(Date()) - consumptionReportingUnit.duration = - utils.calculateTimestampDifferenceInSeconds( - consumptionReportingUnit.startTime, - currentTime - ).toInt() - } - } - - val consumptionReport = ConsumptionReport( - mediaPlayerEntry, - reportingClientId, - consumptionReportingUnitList - ) - val objectMapper: ObjectMapper = jacksonObjectMapper() - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) - - // Filter the values according to the current configuration - val propertiesToIgnore = mutableListOf() - if (playbackConsumptionReportingConfiguration?.locationReporting == false) { - propertiesToIgnore.add("locations") - } - if (playbackConsumptionReportingConfiguration?.accessReporting == false) { - propertiesToIgnore.add("clientEndpointAddress") - propertiesToIgnore.add("serverEndpointAddress") - } - - val filters = ConsumptionReportingFilterProvider.createFilter(propertiesToIgnore) - - return objectMapper.writer(filters).writeValueAsString(consumptionReport) - } - - private fun requestCellInfoUpdates() { - val telephonyManager = - context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - - // Register the cell info callback - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.ACCESS_FINE_LOCATION - ) == PackageManager.PERMISSION_GRANTED - ) { - telephonyManager.requestCellInfoUpdate(context.mainExecutor, cellInfoCallback) - } - } - - /** - * Removes all entries in the consumption report that are finished - * - */ - fun cleanConsumptionReportingList() { - consumptionReportingUnitList.removeIf { obj -> obj.finished } - } -} - -class ConsumptionReportingUnitFilter(private val propertiesToIgnore: List) : - PropertyFilter { - override fun serializeAsField( - pojo: Any?, - gen: JsonGenerator?, - prov: SerializerProvider?, - writer: PropertyWriter? - ) { - if (include(writer)) { - writer?.serializeAsField(pojo, gen, prov) - } else if (!gen?.canOmitFields()!!) { - writer?.serializeAsOmittedField(pojo, gen, prov) - } - } - - override fun serializeAsElement( - elementValue: Any?, - gen: JsonGenerator?, - prov: SerializerProvider?, - writer: PropertyWriter? - ) { - if (include(writer)) { - writer?.serializeAsElement(elementValue, gen, prov) - } else if (!gen?.canOmitFields()!!) { - writer?.serializeAsOmittedField(elementValue, gen, prov) - } - } - - @Deprecated("Deprecated in Java") - override fun depositSchemaProperty( - writer: PropertyWriter?, - propertiesNode: ObjectNode?, - provider: SerializerProvider? - ) { - } - - override fun depositSchemaProperty( - writer: PropertyWriter?, - objectVisitor: JsonObjectFormatVisitor?, - provider: SerializerProvider? - ) { - } - - private fun include(writer: PropertyWriter?): Boolean { - return writer?.name !in propertiesToIgnore - } -} - -object ConsumptionReportingFilterProvider { - fun createFilter(propertiesToIgnore: List): SimpleFilterProvider { - return SimpleFilterProvider().addFilter( - "consumptionReportingUnitFilter", - ConsumptionReportingUnitFilter(propertiesToIgnore) - ) - } -} diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/ConsumptionReportingController.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/ConsumptionReportingController.kt new file mode 100644 index 0000000..d35a465 --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/ConsumptionReportingController.kt @@ -0,0 +1,202 @@ +package com.fivegmag.a5gmsmediastreamhandler.controller + +import android.os.Bundle +import android.os.Message +import android.telephony.* +import androidx.media3.common.util.UnstableApi +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.databind.ser.PropertyFilter +import com.fasterxml.jackson.databind.ser.PropertyWriter +import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider +import com.fivegmag.a5gmscommonlibrary.consumptionReporting.ConsumptionRequest +import com.fivegmag.a5gmscommonlibrary.eventbus.CellInfoUpdatedEvent +import com.fivegmag.a5gmscommonlibrary.eventbus.PlaybackStateChangedEvent +import com.fivegmag.a5gmscommonlibrary.helpers.PlayerStates +import com.fivegmag.a5gmscommonlibrary.helpers.SessionHandlerMessageTypes +import com.fivegmag.a5gmscommonlibrary.session.PlaybackRequest +import com.fivegmag.a5gmsmediastreamhandler.player.ConsumptionReporter +import com.fivegmag.a5gmsmediastreamhandler.player.exoplayer.ConsumptionReporterExoplayer +import com.fivegmag.a5gmsmediastreamhandler.player.exoplayer.IExoPlayerAdapter +import com.fivegmag.a5gmsmediastreamhandler.service.OutgoingMessageHandler +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +@UnstableApi +class ConsumptionReportingController( + private val exoPlayerAdapter: IExoPlayerAdapter, + private val outgoingMessageHandler: OutgoingMessageHandler +) : IConsumptionReportingController { + companion object { + const val TAG = "5GMS-ConsumptionReportingController" + } + + lateinit var reportingClientId: String + private val activeConsumptionReporter = mutableListOf() + private var lastConsumptionRequest: ConsumptionRequest? = + null + + + override fun initialize() { + EventBus.getDefault().register(this) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @UnstableApi + override fun onPlaybackStateChangedEvent(event: PlaybackStateChangedEvent) { + if (event.playbackState == PlayerStates.ENDED) { + triggerConsumptionReport() + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @UnstableApi + override fun onCellInfoUpdatedEvent(event: CellInfoUpdatedEvent) { + if (lastConsumptionRequest != null && lastConsumptionRequest!!.locationReporting == true) { + triggerConsumptionReport() + } + } + + override fun triggerConsumptionReport() { + if (lastConsumptionRequest == null) { + return + } + for (consumptionReporter in activeConsumptionReporter) { + val consumptionReport = consumptionReporter.getConsumptionReport( + reportingClientId, + lastConsumptionRequest!! + ) + sendConsumptionReport(consumptionReport) + consumptionReporter.resetState() + } + } + + private fun sendConsumptionReport(consumptionReport: String) { + val bundle = Bundle() + bundle.putString("consumptionReport", consumptionReport) + outgoingMessageHandler.sendMessageByTypeAndBundle( + SessionHandlerMessageTypes.CONSUMPTION_REPORT, + bundle + ) + } + + @UnstableApi + override fun handleTriggerPlayback(playbackRequest: PlaybackRequest) { + resetState() + setDefaultExoplayerConsumptionReporter() + setLastConsumptionRequest( + playbackRequest.consumptionRequest + ) + } + + private fun setDefaultExoplayerConsumptionReporter() { + val consumptionReporterExoplayer = ConsumptionReporterExoplayer(exoPlayerAdapter) + consumptionReporterExoplayer.initialize() + activeConsumptionReporter.add(consumptionReporterExoplayer) + } + + private fun setLastConsumptionRequest(consumptionRequest: ConsumptionRequest) { + lastConsumptionRequest = consumptionRequest + } + + override fun updateLastConsumptionRequest(msg: Message) { + val consumptionRequest = + getConsumptionRequestFromMessage(msg) + + if (consumptionRequest != null) { + setLastConsumptionRequest( + consumptionRequest + ) + } + } + + private fun getConsumptionRequestFromMessage(msg: Message): ConsumptionRequest? { + val bundle: Bundle = msg.data + bundle.classLoader = ConsumptionRequest::class.java.classLoader + return bundle.getParcelable("consumptionRequest") + } + + override fun handleGetConsumptionReport(msg: Message) { + val consumptionRequest = + getConsumptionRequestFromMessage(msg) + + if (consumptionRequest != null) { + setLastConsumptionRequest( + consumptionRequest + ) + } + triggerConsumptionReport() + } + + override fun reset() { + resetState() + } + + override fun resetState() { + lastConsumptionRequest = null + for (reporter in activeConsumptionReporter) { + reporter.reset() + } + activeConsumptionReporter.clear() + } +} + +class ConsumptionReportingUnitFilter(private val propertiesToIgnore: List) : + PropertyFilter { + override fun serializeAsField( + pojo: Any?, + gen: JsonGenerator?, + prov: SerializerProvider?, + writer: PropertyWriter? + ) { + if (include(writer)) { + writer?.serializeAsField(pojo, gen, prov) + } else if (!gen?.canOmitFields()!!) { + writer?.serializeAsOmittedField(pojo, gen, prov) + } + } + + override fun serializeAsElement( + elementValue: Any?, + gen: JsonGenerator?, + prov: SerializerProvider?, + writer: PropertyWriter? + ) { + if (include(writer)) { + writer?.serializeAsElement(elementValue, gen, prov) + } else if (!gen?.canOmitFields()!!) { + writer?.serializeAsOmittedField(elementValue, gen, prov) + } + } + + @Deprecated("Deprecated in Java") + override fun depositSchemaProperty( + writer: PropertyWriter?, + propertiesNode: ObjectNode?, + provider: SerializerProvider? + ) { + } + + override fun depositSchemaProperty( + writer: PropertyWriter?, + objectVisitor: JsonObjectFormatVisitor?, + provider: SerializerProvider? + ) { + } + + private fun include(writer: PropertyWriter?): Boolean { + return writer?.name !in propertiesToIgnore + } +} + +object ConsumptionReportingFilterProvider { + fun createFilter(propertiesToIgnore: List): SimpleFilterProvider { + return SimpleFilterProvider().addFilter( + "consumptionReportingUnitFilter", + ConsumptionReportingUnitFilter(propertiesToIgnore) + ) + } +} diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/IConsumptionReportingController.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/IConsumptionReportingController.kt new file mode 100644 index 0000000..58d2999 --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/IConsumptionReportingController.kt @@ -0,0 +1,21 @@ +package com.fivegmag.a5gmsmediastreamhandler.controller + +import android.os.Message +import androidx.media3.common.util.UnstableApi +import com.fivegmag.a5gmscommonlibrary.eventbus.CellInfoUpdatedEvent +import com.fivegmag.a5gmscommonlibrary.eventbus.PlaybackStateChangedEvent +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +interface IConsumptionReportingController : IController { + @Subscribe(threadMode = ThreadMode.MAIN) + @UnstableApi + fun onPlaybackStateChangedEvent(event: PlaybackStateChangedEvent) + + @Subscribe(threadMode = ThreadMode.MAIN) + @UnstableApi + fun onCellInfoUpdatedEvent(event: CellInfoUpdatedEvent) + fun triggerConsumptionReport() + fun updateLastConsumptionRequest(msg: Message) + fun handleGetConsumptionReport(msg: Message) +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/IController.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/IController.kt new file mode 100644 index 0000000..74b556b --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/IController.kt @@ -0,0 +1,15 @@ +package com.fivegmag.a5gmsmediastreamhandler.controller + +import com.fivegmag.a5gmscommonlibrary.session.PlaybackRequest + +interface IController { + + fun reset() + + fun resetState() + + fun initialize() + + fun handleTriggerPlayback(playbackRequest: PlaybackRequest) + +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/IQoEMetricsReportingController.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/IQoEMetricsReportingController.kt new file mode 100644 index 0000000..a01ef61 --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/IQoEMetricsReportingController.kt @@ -0,0 +1,17 @@ +package com.fivegmag.a5gmsmediastreamhandler.controller + +import android.os.Message +import androidx.media3.common.util.UnstableApi +import com.fivegmag.a5gmscommonlibrary.eventbus.PlaybackStateChangedEvent +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +interface IQoEMetricsReportingController : IController { + + fun triggerQoeMetricsReports() + + @Subscribe(threadMode = ThreadMode.MAIN) + @UnstableApi + fun onPlaybackStateChangedEvent(event: PlaybackStateChangedEvent) + fun handleGetQoeMetricsReport(msg: Message) +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/ISessionController.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/ISessionController.kt new file mode 100644 index 0000000..1664bb4 --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/ISessionController.kt @@ -0,0 +1,14 @@ +package com.fivegmag.a5gmsmediastreamhandler.controller + +import android.telephony.TelephonyManager +import androidx.media3.common.util.UnstableApi +import com.fivegmag.a5gmscommonlibrary.eventbus.PlaybackStateChangedEvent +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +interface ISessionController : IController { + + @Subscribe(threadMode = ThreadMode.MAIN) + @UnstableApi + fun onPlaybackStateChangedEvent(event: PlaybackStateChangedEvent) +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/QoEMetricsReportingController.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/QoEMetricsReportingController.kt new file mode 100644 index 0000000..d0e5571 --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/QoEMetricsReportingController.kt @@ -0,0 +1,197 @@ +package com.fivegmag.a5gmsmediastreamhandler.controller + +import android.os.Bundle +import android.os.Message +import android.util.Log +import androidx.media3.common.util.UnstableApi +import com.fivegmag.a5gmscommonlibrary.eventbus.PlaybackStateChangedEvent +import com.fivegmag.a5gmscommonlibrary.helpers.PlayerStates +import com.fivegmag.a5gmscommonlibrary.helpers.SessionHandlerMessageTypes +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.QoeMetricsRequest +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.QoeMetricsResponse +import com.fivegmag.a5gmscommonlibrary.session.PlaybackRequest +import com.fivegmag.a5gmsmediastreamhandler.MediaSessionHandlerAdapter +import com.fivegmag.a5gmsmediastreamhandler.player.exoplayer.IExoPlayerAdapter +import com.fivegmag.a5gmsmediastreamhandler.player.exoplayer.QoeMetricsReporterExoplayer +import com.fivegmag.a5gmsmediastreamhandler.service.OutgoingMessageHandler +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import kotlin.reflect.KClass +import kotlin.reflect.full.primaryConstructor + +@UnstableApi +class QoEMetricsReportingController( + private val exoPlayerAdapter: IExoPlayerAdapter, + private val outgoingMessageHandler: OutgoingMessageHandler +) : IQoEMetricsReportingController { + + companion object { + const val TAG = "5GMS-QoEMetricsReportingController" + } + + lateinit var reportingClientId: String + private lateinit var recordingSessionId: String + private val availableQoeMetricsReporterExoplayerByScheme = + mutableMapOf>() + private val activeQoeMetricsReporterById = mutableMapOf() + private var lastQoeMetricsRequestsById = mutableMapOf() + + override fun initialize() { + EventBus.getDefault().register(this) + registerExoplayerQoeMetricsReporter() + } + + private fun registerExoplayerQoeMetricsReporter() { + val scheme = QoeMetricsReporterExoplayer.SCHEME + availableQoeMetricsReporterExoplayerByScheme[scheme] = QoeMetricsReporterExoplayer::class + } + + override fun handleTriggerPlayback(playbackRequest: PlaybackRequest) { + resetState() + recordingSessionId = playbackRequest.mediaStreamingSessionIdentifier + setLastQoeMetricsRequests( + playbackRequest.qoeMetricsRequests + ) + initializeQoeMetricsReporter(playbackRequest.qoeMetricsRequests) + } + + private fun initializeQoeMetricsReporter(qoeMetricsRequests: ArrayList) { + for (qoeMetricsRequest in qoeMetricsRequests) { + initializeQoeMetricsReporterForConfigurationId(qoeMetricsRequest) + } + } + + private fun initializeQoeMetricsReporterForConfigurationId(qoeMetricsRequest: QoeMetricsRequest) { + val qoeMetricsReporterForConfigurationId = + activeQoeMetricsReporterById[qoeMetricsRequest.metricsReportingConfigurationId] + + if (qoeMetricsReporterForConfigurationId != null) { + return + } + + val qoeMetricsReporterExoplayerForScheme = + availableQoeMetricsReporterExoplayerByScheme[qoeMetricsRequest.scheme] ?: return + val instance = + qoeMetricsReporterExoplayerForScheme.primaryConstructor?.call(exoPlayerAdapter) + instance?.initialize(qoeMetricsRequest) + + if (instance != null) { + activeQoeMetricsReporterById[qoeMetricsRequest.metricsReportingConfigurationId] = + instance + } + } + + override fun triggerQoeMetricsReports() { + try { + if (lastQoeMetricsRequestsById.isEmpty()) { + return + } + for (qoeMetricsRequest in lastQoeMetricsRequestsById.values) { + triggerSingleQoeMetricsReport(qoeMetricsRequest) + } + } catch (e: Exception) { + Log.d(TAG, e.message.toString()) + } + } + + private fun triggerSingleQoeMetricsReport(qoeMetricsRequest: QoeMetricsRequest) { + try { + val qoeMetricsReport = getQoeMetricsReport(qoeMetricsRequest) + sendQoeMetricsReport( + qoeMetricsReport, + qoeMetricsRequest.metricsReportingConfigurationId + ) + } catch (e: Exception) { + Log.d(TAG, e.message.toString()) + } + } + + private fun setLastQoeMetricsRequests(qoeMetricsRequests: ArrayList) { + for (qoeMetricsRequest in qoeMetricsRequests) { + lastQoeMetricsRequestsById[qoeMetricsRequest.metricsReportingConfigurationId] = + qoeMetricsRequest + } + } + + private fun setLastQoeMetricsRequestForConfigurationId(qoeMetricsRequest: QoeMetricsRequest) { + lastQoeMetricsRequestsById[qoeMetricsRequest.metricsReportingConfigurationId] = + qoeMetricsRequest + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @UnstableApi + override fun onPlaybackStateChangedEvent(event: PlaybackStateChangedEvent) { + if (event.playbackState == PlayerStates.ENDED) { + triggerQoeMetricsReports() + } + } + + private fun getQoeMetricsReport(qoeMetricsRequest: QoeMetricsRequest?): String { + if (qoeMetricsRequest == null) { + throw Exception("qoeMetricsRequest can not be null") + } + + val qoeMetricsReporterForConfigurationId = + activeQoeMetricsReporterById[qoeMetricsRequest.metricsReportingConfigurationId] + ?: throw Exception("No valid QoE Metrics Reporter for configuration ID ${qoeMetricsRequest.metricsReportingConfigurationId}") + + qoeMetricsReporterForConfigurationId.setLastQoeMetricsRequest(qoeMetricsRequest) + val qoeMetricsReport = + qoeMetricsReporterForConfigurationId.getQoeMetricsReport( + qoeMetricsRequest, + reportingClientId, + recordingSessionId + ) + qoeMetricsReporterForConfigurationId.resetState() + + return qoeMetricsReport + } + + private fun sendQoeMetricsReport( + qoeMetricsReport: String, + metricsReportingConfigurationId: String + ) { + val bundle = Bundle() + val playbackMetricsResponse = QoeMetricsResponse( + qoeMetricsReport, + metricsReportingConfigurationId + ) + bundle.putParcelable("qoeMetricsResponse", playbackMetricsResponse) + + outgoingMessageHandler.sendMessageByTypeAndBundle( + SessionHandlerMessageTypes.REPORT_QOE_METRICS, + bundle + ) + } + + override fun handleGetQoeMetricsReport(msg: Message) { + val bundle: Bundle = msg.data + bundle.classLoader = QoeMetricsRequest::class.java.classLoader + val qoeMetricsRequest: QoeMetricsRequest? = bundle.getParcelable("qoeMetricsRequest") + val scheme = qoeMetricsRequest?.scheme + + Log.d( + MediaSessionHandlerAdapter.TAG, + "Media Session Handler requested QoE metrics for scheme $scheme" + ) + + if (qoeMetricsRequest != null) { + triggerSingleQoeMetricsReport(qoeMetricsRequest) + setLastQoeMetricsRequestForConfigurationId(qoeMetricsRequest) + } + } + + override fun reset() { + resetState() + } + + override fun resetState() { + for (reporter in activeQoeMetricsReporterById.values) { + reporter.reset() + } + activeQoeMetricsReporterById.clear() + lastQoeMetricsRequestsById.clear() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/SessionController.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/SessionController.kt new file mode 100644 index 0000000..158efb5 --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/controller/SessionController.kt @@ -0,0 +1,82 @@ +package com.fivegmag.a5gmsmediastreamhandler.controller + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.telephony.CellInfo +import android.telephony.TelephonyManager +import androidx.core.app.ActivityCompat +import androidx.media3.common.util.UnstableApi +import com.fivegmag.a5gmscommonlibrary.eventbus.CellInfoUpdatedEvent +import com.fivegmag.a5gmscommonlibrary.eventbus.PlaybackStateChangedEvent +import com.fivegmag.a5gmscommonlibrary.helpers.ContentTypes +import com.fivegmag.a5gmscommonlibrary.models.EntryPoint +import com.fivegmag.a5gmscommonlibrary.session.PlaybackRequest +import com.fivegmag.a5gmsmediastreamhandler.player.exoplayer.IExoPlayerAdapter +import com.fivegmag.a5gmsmediastreamhandler.service.OutgoingMessageHandler +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +class SessionController( + private val context: Context, + private val exoPlayerAdapter: IExoPlayerAdapter, + private val outgoingMessageHandler: OutgoingMessageHandler +) : ISessionController { + + private val cellInfoCallback = object : TelephonyManager.CellInfoCallback() { + override fun onCellInfo(cellInfoList: MutableList) { + EventBus.getDefault().post(CellInfoUpdatedEvent(cellInfoList)) + } + } + + override fun initialize() { + EventBus.getDefault().register(this) + requestCellInfoUpdates() + } + + @UnstableApi + override fun handleTriggerPlayback(playbackRequest: PlaybackRequest) { + if (playbackRequest.entryPoints.size > 0) { + val dashEntryPoints: List = + playbackRequest.entryPoints.filter { entryPoint -> entryPoint.contentType == ContentTypes.DASH } + + if (dashEntryPoints.isNotEmpty()) { + val mpdUrl = dashEntryPoints[0].locator + exoPlayerAdapter.attach(mpdUrl, ContentTypes.DASH) + exoPlayerAdapter.preload() + exoPlayerAdapter.play() + } + } + } + + private fun requestCellInfoUpdates() { + val telephonyManager = + context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + + // Register the cell info callback + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + telephonyManager.requestCellInfoUpdate(context.mainExecutor, cellInfoCallback) + } + } + + + @Subscribe(threadMode = ThreadMode.MAIN) + @UnstableApi + override fun onPlaybackStateChangedEvent(event: PlaybackStateChangedEvent) { + outgoingMessageHandler.updatePlaybackState(event.playbackState) + } + + override fun resetState() { + + } + + override fun reset() { + resetState() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/ConsumptionReporter.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/ConsumptionReporter.kt new file mode 100644 index 0000000..be95820 --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/ConsumptionReporter.kt @@ -0,0 +1,68 @@ +package com.fivegmag.a5gmsmediastreamhandler.player + +import android.telephony.CellIdentityGsm +import android.telephony.CellIdentityLte +import android.telephony.CellIdentityNr +import android.telephony.CellInfo +import android.telephony.CellInfoGsm +import android.telephony.CellInfoLte +import android.telephony.CellInfoNr +import com.fivegmag.a5gmscommonlibrary.consumptionReporting.ConsumptionRequest +import com.fivegmag.a5gmscommonlibrary.models.CellIdentifierType +import com.fivegmag.a5gmscommonlibrary.models.TypedLocation +import java.lang.Exception + +abstract class ConsumptionReporter { + + abstract fun initialize() + + abstract fun getConsumptionReport( + reportingClientId: String, + consumptionRequest: ConsumptionRequest + ): String + + abstract fun resetState() + + abstract fun reset() + + fun createTypedLocationByCellInfo(cellInfo: CellInfo): TypedLocation? { + try { + val typedLocation: TypedLocation? + when (cellInfo) { + // CGI = MCC + MNC + LAC + CI + is CellInfoGsm -> { + val cellIdentity = cellInfo.cellIdentity as CellIdentityGsm + val mcc = cellIdentity.mccString + val mnc = cellIdentity.mncString + val lac = cellIdentity.lac + val ci = cellIdentity.cid + typedLocation = TypedLocation(CellIdentifierType.CGI, "$mcc$mnc$lac$ci") + } + // ECGI = MCC + MNC + ECI + is CellInfoLte -> { + val cellIdentity = cellInfo.cellIdentity as CellIdentityLte + val mcc = cellIdentity.mccString + val mnc = cellIdentity.mncString + val eci = cellIdentity.ci + typedLocation = TypedLocation(CellIdentifierType.ECGI, "$mcc$mnc$eci") + } + // NCGI = MCC + MNC + NCI + is CellInfoNr -> { + val cellIdentity = cellInfo.cellIdentity as CellIdentityNr + val mcc = cellIdentity.mccString + val mnc = cellIdentity.mncString + val nci = cellIdentity.nci + typedLocation = TypedLocation(CellIdentifierType.NCGI, "$mcc$mnc$nci") + } + + else -> { + return null + } + } + + return typedLocation + } catch (e: Exception) { + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/IQoeMetricsReporter.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/IQoeMetricsReporter.kt new file mode 100644 index 0000000..5cde814 --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/IQoeMetricsReporter.kt @@ -0,0 +1,20 @@ +package com.fivegmag.a5gmsmediastreamhandler.player + +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.QoeMetricsRequest + +interface IQoeMetricsReporter { + + fun initialize(lastQoeMetricsRequest: QoeMetricsRequest) + + fun getQoeMetricsReport( + qoeMetricsRequest: QoeMetricsRequest, + reportingClientId: String, + recordingSessionId: String + ): String + + fun reset() + + fun resetState() + + fun setLastQoeMetricsRequest(lastQoeMetricsRequest: QoeMetricsRequest) +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ConsumptionReporterExoplayer.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ConsumptionReporterExoplayer.kt new file mode 100644 index 0000000..7c58ff0 --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ConsumptionReporterExoplayer.kt @@ -0,0 +1,203 @@ +package com.fivegmag.a5gmsmediastreamhandler.player.exoplayer + +import android.annotation.SuppressLint +import android.os.Build +import android.telephony.CellInfo +import androidx.annotation.RequiresApi +import androidx.media3.common.util.Log +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.source.MediaLoadData +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fivegmag.a5gmscommonlibrary.consumptionReporting.ConsumptionReport +import com.fivegmag.a5gmscommonlibrary.consumptionReporting.ConsumptionReportingUnit +import com.fivegmag.a5gmscommonlibrary.consumptionReporting.ConsumptionRequest +import com.fivegmag.a5gmscommonlibrary.eventbus.CellInfoUpdatedEvent +import com.fivegmag.a5gmscommonlibrary.eventbus.DownstreamFormatChangedEvent +import com.fivegmag.a5gmscommonlibrary.eventbus.LoadStartedEvent +import com.fivegmag.a5gmscommonlibrary.helpers.Utils +import com.fivegmag.a5gmscommonlibrary.models.EndpointAddress +import com.fivegmag.a5gmscommonlibrary.models.TypedLocation +import com.fivegmag.a5gmsmediastreamhandler.controller.ConsumptionReportingFilterProvider +import com.fivegmag.a5gmsmediastreamhandler.player.ConsumptionReporter +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.lang.Exception +import java.util.Date + +class ConsumptionReporterExoplayer( + private val exoPlayerAdapter: IExoPlayerAdapter +) : ConsumptionReporter() { + + companion object { + const val TAG = "5GMS-ConsumptionReporterExoplayer" + } + + private val utils: Utils = Utils() + private val consumptionReportingUnitList: ArrayList = ArrayList() + private var serverEndpointAddressesPerMediaType = mutableMapOf() + private var activeLocations: ArrayList = ArrayList() + override fun initialize() { + EventBus.getDefault().register(this) + } + + + @RequiresApi(Build.VERSION_CODES.R) + @Subscribe(threadMode = ThreadMode.MAIN) + @UnstableApi + fun onDownstreamFormatChangedEvent(event: DownstreamFormatChangedEvent) { + addConsumptionReportingUnit(event.mediaLoadData) + } + + @SuppressLint("Range") + @Subscribe(threadMode = ThreadMode.MAIN) + @UnstableApi + fun onLoadStartedEvent(event: LoadStartedEvent) { + try { + val mimeType = event.mediaLoadData.trackFormat!!.containerMimeType + if (mimeType != null) { + val requestUrl = event.loadEventInfo.dataSpec.uri.toString() + val endpointAddress = utils.getEndpointAddressByRequestUrl(requestUrl) + if (endpointAddress != null) { + serverEndpointAddressesPerMediaType[mimeType] = endpointAddress + } + } + } catch (e: Exception) { + Log.d(TAG, "Error while creating server endpoint address: $e") + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @UnstableApi + fun onCellInfoUpdated(event: CellInfoUpdatedEvent) { + activeLocations.clear() + for (cellInfo in event.cellInfoList) { + if (cellInfo.isRegistered && (cellInfo.cellConnectionStatus == CellInfo.CONNECTION_PRIMARY_SERVING || cellInfo.cellConnectionStatus == CellInfo.CONNECTION_SECONDARY_SERVING)) { + val location = createTypedLocationByCellInfo(cellInfo) + if (location != null) { + activeLocations.add(location) + } + } + } + } + + @SuppressLint("Range") + @RequiresApi(Build.VERSION_CODES.R) + @UnstableApi + private fun addConsumptionReportingUnit(mediaLoadData: MediaLoadData) { + val startTime = utils.formatDateToOpenAPIFormat(Date()) + val mediaConsumed = mediaLoadData.trackFormat?.id + val mimeType = mediaLoadData.trackFormat!!.containerMimeType + val duration = 0 + + // If we have a previous entry in the list of consumption reporting units with the same media type then we can calculate the duration now + val existingEntry = consumptionReportingUnitList.find { it.mimeType == mimeType } + if (existingEntry != null) { + existingEntry.duration = + utils.calculateTimestampDifferenceInSeconds(existingEntry.startTime, startTime) + .toInt() + existingEntry.finished = true + } + + // Add the new entry + val consumptionReportingUnit = + mediaConsumed?.let { + ConsumptionReportingUnit( + it, + null, + null, + startTime, + duration, + null + ) + } + if (consumptionReportingUnit != null) { + consumptionReportingUnit.mimeType = mimeType + + // We add locations and clientEndpointAddress and filter the attributes later in case the corresponding configuration options are set to false + // If we do not add the information here we can not include it later in case the SAI configuration changes + consumptionReportingUnit.locations = activeLocations + consumptionReportingUnit.serverEndpointAddress = getServerEndpointAddress(mimeType) + consumptionReportingUnit.clientEndpointAddress = + getClientEndpointAddress(consumptionReportingUnit.serverEndpointAddress) + consumptionReportingUnitList.add(consumptionReportingUnit) + } + } + + @UnstableApi + override fun getConsumptionReport( + reportingClientId: String, + consumptionRequest: ConsumptionRequest + ): String { + val mediaPlayerEntry = exoPlayerAdapter.getCurrentManifestUri() + // We need to add the duration of the consumption reporting units that are not yet finished + for (consumptionReportingUnit in consumptionReportingUnitList) { + if (!consumptionReportingUnit.finished) { + val currentTime = utils.formatDateToOpenAPIFormat(Date()) + consumptionReportingUnit.duration = + utils.calculateTimestampDifferenceInSeconds( + consumptionReportingUnit.startTime, + currentTime + ).toInt() + } + } + + val consumptionReport = ConsumptionReport( + mediaPlayerEntry, + reportingClientId, + consumptionReportingUnitList + ) + val objectMapper: ObjectMapper = jacksonObjectMapper() + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) + + // Filter the values according to the current configuration + val propertiesToIgnore = mutableListOf() + if (consumptionRequest.locationReporting == false) { + propertiesToIgnore.add("locations") + } + if (consumptionRequest.accessReporting == false) { + propertiesToIgnore.add("clientEndpointAddress") + propertiesToIgnore.add("serverEndpointAddress") + } + + val filters = ConsumptionReportingFilterProvider.createFilter(propertiesToIgnore) + + return objectMapper.writer(filters).writeValueAsString(consumptionReport) + } + + private fun getClientEndpointAddress(serverEndpointAddress: EndpointAddress?): EndpointAddress { + val ipv4Address = utils.getIpAddress(4) + val ipv6Address = utils.getIpAddress(6) + var portNumber = 80 + if (serverEndpointAddress != null) { + portNumber = serverEndpointAddress.portNumber + } + + return EndpointAddress(null, ipv4Address, ipv6Address, portNumber) + } + + private fun cleanConsumptionReportingList() { + consumptionReportingUnitList.removeIf { obj -> obj.finished } + } + + private fun getServerEndpointAddress(mimeType: String?): EndpointAddress? { + if (mimeType == null) { + return null + } + + return serverEndpointAddressesPerMediaType[mimeType] ?: return null + } + + override fun resetState() { + cleanConsumptionReportingList() + serverEndpointAddressesPerMediaType.clear() + } + + override fun reset() { + resetState() + consumptionReportingUnitList.clear() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/ExoPlayerAdapter.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ExoPlayerAdapter.kt similarity index 76% rename from app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/ExoPlayerAdapter.kt rename to app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ExoPlayerAdapter.kt index 3eb2df1..0235fec 100644 --- a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/ExoPlayerAdapter.kt +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ExoPlayerAdapter.kt @@ -1,13 +1,4 @@ -/* -License: 5G-MAG Public License (v1.0) -Author: Daniel Silhavy -Copyright: (C) 2023 Fraunhofer FOKUS -For full license terms please see the LICENSE file distributed with this -program. If this file is missing then the license can be retrieved from -https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view -*/ - -package com.fivegmag.a5gmsmediastreamhandler +package com.fivegmag.a5gmsmediastreamhandler.player.exoplayer import android.content.Context import androidx.media3.common.MediaItem @@ -19,6 +10,7 @@ import androidx.media3.datasource.DataSource import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.HttpDataSource import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.dash.manifest.DashManifest import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter import androidx.media3.exoplayer.util.EventLogger @@ -27,11 +19,9 @@ import com.fivegmag.a5gmscommonlibrary.helpers.ContentTypes import com.fivegmag.a5gmscommonlibrary.helpers.PlayerStates import com.fivegmag.a5gmscommonlibrary.helpers.StatusInformation import com.fivegmag.a5gmscommonlibrary.helpers.UserAgentTokens -import com.fivegmag.a5gmsmediastreamhandler.helpers.mapStateToConstant - @UnstableApi -class ExoPlayerAdapter() { +class ExoPlayerAdapter() : IExoPlayerAdapter { private lateinit var playerInstance: ExoPlayer private lateinit var playerView: PlayerView @@ -39,13 +29,11 @@ class ExoPlayerAdapter() { private lateinit var activeManifestUrl: String private lateinit var playerListener: ExoPlayerListener private lateinit var bandwidthMeter: DefaultBandwidthMeter - private lateinit var mediaSessionHandlerAdapter: MediaSessionHandlerAdapter - fun initialize( + override fun initialize( exoPlayerView: PlayerView, - context: Context, - msh: MediaSessionHandlerAdapter + context: Context ) { val defaultUserAgent = Util.getUserAgent(context, "A5GMSMediaStreamHandler") val deviceName = android.os.Build.MODEL @@ -60,7 +48,6 @@ class ExoPlayerAdapter() { val dataSource = httpDataSourceFactory.createDataSource() dataSource } - mediaSessionHandlerAdapter = msh playerInstance = ExoPlayer.Builder(context) .setMediaSourceFactory( DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory) @@ -75,7 +62,7 @@ class ExoPlayerAdapter() { playerInstance.addAnalyticsListener(playerListener) } - fun attach(url: String, contentType: String = "") { + override fun attach(url: String, contentType: String) { val mediaItem: MediaItem when (contentType) { ContentTypes.DASH -> { @@ -102,68 +89,81 @@ class ExoPlayerAdapter() { activeManifestUrl = url } - fun handleSourceChange() { - // Send the final consumption report - if (activeMediaItem != null) { - mediaSessionHandlerAdapter.sendConsumptionReport() - } - playerListener.resetState() - mediaSessionHandlerAdapter.resetState() + override fun hasActiveMediaItem() : Boolean { + return activeMediaItem != null } - fun getCurrentManifestUri(): String { + override fun getCurrentManifestUri(): String { return activeManifestUrl } - fun preload() { + override fun getCurrentManifestUrl(): String { + return playerInstance.currentMediaItem?.localConfiguration?.uri.toString() + } + + override fun preload() { playerInstance.prepare() } - fun play() { + override fun play() { playerInstance.play() } - fun pause() { + override fun pause() { playerInstance.pause() } - fun seek(time: Long) { + override fun seek(time: Long) { TODO("Not yet implemented") } - fun stop() { + override fun stop() { playerInstance.stop() } - fun reset() { + override fun reset() { TODO("Not yet implemented") } - fun destroy() { + override fun destroy() { playerInstance.release() } - fun getPlayerInstance(): ExoPlayer { + override fun getPlayerInstance(): ExoPlayer { return playerInstance } - fun getPlaybackState(): Int { + override fun getPlaybackState(): Int { return playerInstance.playbackState } - private fun getAverageThroughput(): Long { - return bandwidthMeter.bitrateEstimate + override fun getCurrentPosition(): Long { + return playerInstance.currentPosition } - private fun getBufferLength(): Long { + override fun getBufferLength(): Long { return playerInstance.totalBufferedDuration } + override fun getAverageThroughput(): Long { + return bandwidthMeter.bitrateEstimate + } private fun getLiveLatency(): Long { return playerInstance.currentLiveOffset } - fun getStatusInformation(status: String): Any? { + override fun getCurrentPeriodId(): String { + val dashManifest = playerInstance.currentManifest as DashManifest + val periodId = dashManifest.getPeriod(playerInstance.currentPeriodIndex).id + + if (periodId != null) { + return periodId + } + + return "" + } + + override fun getStatusInformation(status: String): Any? { when (status) { StatusInformation.AVERAGE_THROUGHPUT -> return getAverageThroughput() StatusInformation.BUFFER_LENGTH -> return getBufferLength() @@ -174,7 +174,7 @@ class ExoPlayerAdapter() { } } - fun getPlayerState(): String { + override fun getPlayerState(): String { val state: String? if (playerInstance.isPlaying) { state = PlayerStates.PLAYING diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/helpers/ExoPlayerHelper.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ExoPlayerHelper.kt similarity index 88% rename from app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/helpers/ExoPlayerHelper.kt rename to app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ExoPlayerHelper.kt index 51389e6..aa58005 100644 --- a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/helpers/ExoPlayerHelper.kt +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ExoPlayerHelper.kt @@ -1,4 +1,4 @@ -package com.fivegmag.a5gmsmediastreamhandler.helpers +package com.fivegmag.a5gmsmediastreamhandler.player.exoplayer import androidx.media3.common.Player import com.fivegmag.a5gmscommonlibrary.helpers.PlayerStates diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/ExoPlayerListener.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ExoPlayerListener.kt similarity index 75% rename from app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/ExoPlayerListener.kt rename to app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ExoPlayerListener.kt index e559126..9bc91c0 100644 --- a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/ExoPlayerListener.kt +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/ExoPlayerListener.kt @@ -1,13 +1,4 @@ -/* -License: 5G-MAG Public License (v1.0) -Author: Daniel Silhavy -Copyright: (C) 2023 Fraunhofer FOKUS -For full license terms please see the LICENSE file distributed with this -program. If this file is missing then the license can be retrieved from -https://drive.google.com/file/d/1cinCiA778IErENZ3JN52VFW-1ffHpx7Z/view -*/ - -package com.fivegmag.a5gmsmediastreamhandler +package com.fivegmag.a5gmsmediastreamhandler.player.exoplayer import android.os.Build import android.util.Log @@ -21,22 +12,21 @@ import androidx.media3.exoplayer.source.LoadEventInfo import androidx.media3.exoplayer.source.MediaLoadData import androidx.media3.ui.PlayerView import com.fivegmag.a5gmscommonlibrary.eventbus.DownstreamFormatChangedEvent +import com.fivegmag.a5gmscommonlibrary.eventbus.LoadCompletedEvent import com.fivegmag.a5gmscommonlibrary.eventbus.LoadStartedEvent import com.fivegmag.a5gmscommonlibrary.eventbus.PlaybackStateChangedEvent import com.fivegmag.a5gmscommonlibrary.helpers.PlayerStates -import com.fivegmag.a5gmsmediastreamhandler.helpers.mapStateToConstant import org.greenrobot.eventbus.EventBus - -const val TAG = "ExoPlayerListener" - -// See https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.Listener.html for possible events @UnstableApi class ExoPlayerListener( private val playerInstance: ExoPlayer, private val playerView: PlayerView, -) : - AnalyticsListener { +) : AnalyticsListener { + + companion object { + const val TAG = "5GMS-ExoPlayerListener" + } override fun onPlaybackStateChanged( eventTime: AnalyticsListener.EventTime, @@ -45,6 +35,7 @@ class ExoPlayerListener( val state: String = mapStateToConstant(playbackState) playerView.keepScreenOn = !(state == PlayerStates.IDLE || state == PlayerStates.ENDED) + Log.d(TAG, "Playback state changed to $state") EventBus.getDefault().post(PlaybackStateChangedEvent(eventTime, state)) } @@ -76,17 +67,16 @@ class ExoPlayerListener( EventBus.getDefault().post(LoadStartedEvent(eventTime, loadEventInfo, mediaLoadData)) } - override fun onPlayerError(eventTime: AnalyticsListener.EventTime, error: PlaybackException) { - Log.d("ExoPlayer", "Error") + override fun onLoadCompleted( + eventTime: AnalyticsListener.EventTime, + loadEventInfo: LoadEventInfo, + mediaLoadData: MediaLoadData + ) { + EventBus.getDefault().post(LoadCompletedEvent(eventTime, loadEventInfo, mediaLoadData)) } - /** - * Removes all entries from the consumption reporting list - * - */ - - fun resetState() { - Log.d(TAG, "Resetting ExoPlayerListener") + override fun onPlayerError(eventTime: AnalyticsListener.EventTime, error: PlaybackException) { + Log.d("ExoPlayer", "Error") } } \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/IExoPlayerAdapter.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/IExoPlayerAdapter.kt new file mode 100644 index 0000000..0568782 --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/IExoPlayerAdapter.kt @@ -0,0 +1,33 @@ +package com.fivegmag.a5gmsmediastreamhandler.player.exoplayer + +import android.content.Context +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView + +interface IExoPlayerAdapter { + + fun initialize( + exoPlayerView: PlayerView, + context: Context + ) + + fun attach(url: String, contentType: String = "") + fun hasActiveMediaItem() : Boolean + fun getCurrentManifestUri(): String + fun getCurrentManifestUrl(): String + fun preload() + fun play() + fun pause() + fun seek(time: Long) + fun stop() + fun reset() + fun destroy() + fun getPlayerInstance(): ExoPlayer + fun getPlaybackState(): Int + fun getCurrentPosition(): Long + fun getBufferLength(): Long + fun getAverageThroughput(): Long + fun getCurrentPeriodId(): String + fun getStatusInformation(status: String): Any? + fun getPlayerState(): String +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/QoeMetricsReporterExoplayer.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/QoeMetricsReporterExoplayer.kt new file mode 100644 index 0000000..65e01f8 --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/player/exoplayer/QoeMetricsReporterExoplayer.kt @@ -0,0 +1,328 @@ +package com.fivegmag.a5gmsmediastreamhandler.player.exoplayer + +import android.annotation.SuppressLint +import android.os.Handler +import android.os.Looper +import androidx.media3.common.util.Log +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.source.LoadEventInfo +import androidx.media3.exoplayer.source.MediaLoadData +import com.fasterxml.jackson.dataformat.xml.XmlMapper +import com.fivegmag.a5gmscommonlibrary.eventbus.DownstreamFormatChangedEvent +import com.fivegmag.a5gmscommonlibrary.eventbus.LoadCompletedEvent +import com.fivegmag.a5gmscommonlibrary.eventbus.PlaybackStateChangedEvent +import com.fivegmag.a5gmscommonlibrary.helpers.MetricReportingSchemes +import com.fivegmag.a5gmscommonlibrary.helpers.Metrics +import com.fivegmag.a5gmscommonlibrary.helpers.PlayerStates +import com.fivegmag.a5gmscommonlibrary.helpers.Utils +import com.fivegmag.a5gmscommonlibrary.helpers.XmlSchemaStrings +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.BufferLevel +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.BufferLevelEntry +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.HttpList +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.HttpListEntry +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.HttpListEntryType +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.MpdInfo +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.MpdInformation +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.QoeMetricsRequest +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.QoeReport +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.ReceptionReport +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.RepresentationSwitch +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.RepresentationSwitchList +import com.fivegmag.a5gmscommonlibrary.qoeMetricsReporting.Trace +import com.fivegmag.a5gmsmediastreamhandler.player.IQoeMetricsReporter +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.lang.Exception +import java.util.Timer +import java.util.TimerTask + +@UnstableApi +class QoeMetricsReporterExoplayer( + private val exoPlayerAdapter: IExoPlayerAdapter +) : IQoeMetricsReporter { + private val representationSwitchList: RepresentationSwitchList = RepresentationSwitchList( + ArrayList() + ) + private val httpList: HttpList = HttpList(ArrayList()) + private val bufferLevel: BufferLevel = BufferLevel(ArrayList()) + private val mpdInformation: ArrayList = ArrayList() + private val utils: Utils = Utils() + private var lastQoeMetricsRequest: QoeMetricsRequest? = null + private var samplingPeriodTimer: Timer? = null + + companion object { + const val TAG = "5GMS-QoeMetricsReporterExoplayer" + const val SCHEME = MetricReportingSchemes.THREE_GPP_DASH_METRIC_REPORTING + } + + override fun setLastQoeMetricsRequest(lastQoeMetricsRequest: QoeMetricsRequest) { + this.lastQoeMetricsRequest = lastQoeMetricsRequest + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onDownstreamFormatChangedEvent(downstreamFormatChangedEvent: DownstreamFormatChangedEvent) { + val isRepresentationSwitchAdded = + addRepresentationSwitch(downstreamFormatChangedEvent.mediaLoadData) + + if (isRepresentationSwitchAdded) { + addMpdInformation(downstreamFormatChangedEvent.mediaLoadData) + } + } + + private fun addRepresentationSwitch(mediaLoadData: MediaLoadData): Boolean { + val t: String = utils.getCurrentXsDateTime() + val currentPosition = exoPlayerAdapter.getCurrentPosition() + val mt: String? = utils.millisecondsToISO8601(currentPosition) + val to: String? = mediaLoadData.trackFormat?.id + val representationSwitch = to?.let { RepresentationSwitch(t, mt, it) } + + if (representationSwitch != null) { + representationSwitchList.entries.add(representationSwitch) + return true + } + + return false + } + + private fun addMpdInformation(mediaLoadData: MediaLoadData) { + val format = mediaLoadData.trackFormat + if (format != null) { + val representationId = mediaLoadData.trackFormat!!.id + val codecs = mediaLoadData.trackFormat!!.codecs + val bandwidth = mediaLoadData.trackFormat!!.peakBitrate + val mimeType = mediaLoadData.trackFormat!!.containerMimeType + val frameRate = mediaLoadData.trackFormat!!.frameRate + val width = mediaLoadData.trackFormat!!.width + val height = mediaLoadData.trackFormat!!.height + val mpdInfo = MpdInfo(codecs, bandwidth, mimeType) + + if (frameRate > 0) { + mpdInfo.frameRate = frameRate.toDouble() + } + if (width > 0) { + mpdInfo.width = width + } + + if (height > 0) { + mpdInfo.height = height + } + mpdInformation.add(MpdInformation(representationId, null, mpdInfo)) + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onPlaybackStateChangedEvent(playbackStateChangedEvent: PlaybackStateChangedEvent) { + if (playbackStateChangedEvent.playbackState == PlayerStates.BUFFERING) { + addBufferLevelEntry() + } + } + + private fun addBufferLevelEntry() { + val level: Int = exoPlayerAdapter.getBufferLength().toInt() + val time: String = utils.getCurrentXsDateTime() + val entry = BufferLevelEntry(time, level) + bufferLevel.entries.add(entry) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onLoadCompleted( + loadCompletedEvent: LoadCompletedEvent + ) { + addHttpListEntry(loadCompletedEvent.mediaLoadData, loadCompletedEvent.loadEventInfo) + addBufferLevelEntry() + } + + private fun addHttpListEntry(mediaLoadData: MediaLoadData, loadEventInfo: LoadEventInfo) { + val tcpId = null + val type = getRequestType(mediaLoadData) + val url = loadEventInfo.uri.toString() + val actualUrl = loadEventInfo.uri.toString() + val range = "" + val tRequest = + utils.convertTimestampToXsDateTime(utils.getCurrentTimestamp() - loadEventInfo.loadDurationMs) + val tResponse = + utils.convertTimestampToXsDateTime(utils.getCurrentTimestamp() - loadEventInfo.loadDurationMs) + val responseCode = 200 + val interval = loadEventInfo.loadDurationMs.toInt() + val bytes = loadEventInfo.bytesLoaded.toInt() + val trace = Trace( + tResponse, + loadEventInfo.loadDurationMs, + bytes + ) + val traceList = ArrayList() + traceList.add(trace) + val httpListEntry = HttpListEntry( + tcpId, + type, + url, + actualUrl, + range, + tRequest, + tResponse, + responseCode, + interval, + traceList + ) + + httpList.entries.add(httpListEntry) + } + + private fun getRequestType(mediaLoadData: MediaLoadData): String { + return when (mediaLoadData.dataType) { + androidx.media3.common.C.DATA_TYPE_UNKNOWN -> HttpListEntryType.OTHER.value + androidx.media3.common.C.DATA_TYPE_MEDIA -> HttpListEntryType.MEDIA_SEGMENT.value + androidx.media3.common.C.DATA_TYPE_MEDIA_INITIALIZATION -> HttpListEntryType.INITIALIZATION_SEGMENT.value + androidx.media3.common.C.DATA_TYPE_MANIFEST -> HttpListEntryType.MPD.value + else -> HttpListEntryType.OTHER.value + + } + } + + override fun initialize(lastQoeMetricsRequest: QoeMetricsRequest) { + EventBus.getDefault().register(this) + setLastQoeMetricsRequest(lastQoeMetricsRequest) + initializeSamplingPeriodTimer() + } + + @SuppressLint("Range") + override fun getQoeMetricsReport( + qoeMetricsRequest: QoeMetricsRequest, + reportingClientId: String, + recordingSessionId: String + ): String { + try { + val qoeMetricsReport = QoeReport() + qoeMetricsReport.reportTime = utils.getCurrentXsDateTime() + qoeMetricsReport.periodId = exoPlayerAdapter.getCurrentPeriodId() + qoeMetricsReport.reportPeriod = qoeMetricsRequest.reportingInterval?.toInt() + qoeMetricsReport.recordingSessionId = recordingSessionId + + if (shouldReportMetric(Metrics.BUFFER_LEVEL, qoeMetricsRequest.metrics)) { + if (bufferLevel.entries.size > 0) { + qoeMetricsReport.bufferLevel = arrayListOf(bufferLevel) + } + } + + if (shouldReportMetric(Metrics.REP_SWITCH_LIST, qoeMetricsRequest.metrics)) { + if (representationSwitchList.entries.size > 0) { + qoeMetricsReport.representationSwitchList = + arrayListOf(representationSwitchList) + } + } + + if (shouldReportMetric(Metrics.HTTP_LIST, qoeMetricsRequest.metrics)) { + if (httpList.entries.size > 0) { + qoeMetricsReport.httpList = arrayListOf(httpList) + } + } + + if (shouldReportMetric(Metrics.MPD_INFORMATION, qoeMetricsRequest.metrics)) { + if (mpdInformation.size > 0) { + qoeMetricsReport.mpdInformation = mpdInformation + } + } + + val receptionReport = + ReceptionReport(qoeMetricsReport, exoPlayerAdapter.getCurrentManifestUrl()) + receptionReport.xmlns = + XmlSchemaStrings.THREE_GPP_METADATA_2011_HSD_RECEPTION_REPORT.SCHEMA + receptionReport.schemaLocation = + XmlSchemaStrings.THREE_GPP_METADATA_2011_HSD_RECEPTION_REPORT.SCHEMA + " " + XmlSchemaStrings.THREE_GPP_METADATA_2011_HSD_RECEPTION_REPORT.LOCATION + receptionReport.xsi = XmlSchemaStrings.THREE_GPP_METADATA_2011_HSD_RECEPTION_REPORT.XSI + receptionReport.sv = XmlSchemaStrings.THREE_GPP_METADATA_2011_HSD_RECEPTION_REPORT.SV + receptionReport.clientId = reportingClientId + + var xml = serializeReceptionReportToXml(receptionReport) + xml = addDelimiter(xml) + + return xml + } catch (e: Exception) { + Log.e(TAG, e.message.toString()) + return "" + } + } + + private fun initializeSamplingPeriodTimer() { + if (samplingPeriodTimer != null) { + return + } + val timer = Timer() + val samplingPeriod = lastQoeMetricsRequest?.samplingPeriod?.times(1000) + if (samplingPeriod != null) { + timer.scheduleAtFixedRate( + object : TimerTask() { + @SuppressLint("Range") + override fun run() { + Log.d(TAG, "Adding BufferLevelEntry due to sampling period timer") + Handler(Looper.getMainLooper()).post { + addBufferLevelEntry() + } + } + }, + 0, + samplingPeriod + ) + } + samplingPeriodTimer = timer + } + + private fun stopSamplingPeriodTimer() { + if (samplingPeriodTimer != null) { + samplingPeriodTimer!!.cancel() + samplingPeriodTimer = null + } + } + + + private fun shouldReportMetric(metric: String, metricsList: ArrayList?): Boolean { + // Special handling for HTTP List. Needs to be enabled explicitly as not part of TS 26.247 + if (metric == Metrics.HTTP_LIST) { + if (metricsList != null) { + return metricsList.contains(metric) + } + } + return metricsList.isNullOrEmpty() || metricsList.contains(metric) + } + + private fun addDelimiter(xmlString: String): String { + val delimiter = "0" + val qoeMetricEndTag = "" + + // Find the last occurrence of the tag + val lastIndex = xmlString.lastIndexOf(qoeMetricEndTag) + if (lastIndex != -1) { + // Insert the delimiter after the last tag + val stringBuilder = StringBuilder(xmlString) + stringBuilder.insert(lastIndex + qoeMetricEndTag.length, delimiter) + return stringBuilder.toString() + } + + // If the tag is not found, return the original XML string + return xmlString + } + + @SuppressLint("Range") + private fun serializeReceptionReportToXml(input: ReceptionReport): String { + val xmlMapper = XmlMapper() + val serializedResult = xmlMapper.writeValueAsString(input) + + return "$serializedResult" + } + + override fun reset() { + resetState() + lastQoeMetricsRequest = null + stopSamplingPeriodTimer() + } + + @SuppressLint("Range") + override fun resetState() { + representationSwitchList.entries.clear() + httpList.entries.clear() + bufferLevel.entries.clear() + mpdInformation.clear() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/service/IncomingMessageHandler.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/service/IncomingMessageHandler.kt new file mode 100644 index 0000000..65e53ee --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/service/IncomingMessageHandler.kt @@ -0,0 +1,93 @@ +package com.fivegmag.a5gmsmediastreamhandler.service + +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.os.Messenger +import androidx.media3.common.util.UnstableApi +import com.fivegmag.a5gmscommonlibrary.helpers.SessionHandlerMessageTypes +import com.fivegmag.a5gmscommonlibrary.session.PlaybackRequest +import com.fivegmag.a5gmsmediastreamhandler.controller.IConsumptionReportingController +import com.fivegmag.a5gmsmediastreamhandler.controller.IQoEMetricsReportingController +import com.fivegmag.a5gmsmediastreamhandler.controller.ISessionController + +class IncomingMessageHandler() { + + private lateinit var consumptionReportingController: IConsumptionReportingController + private lateinit var qoeMetricsReportingController: IQoEMetricsReportingController + private lateinit var sessionController: ISessionController + + @UnstableApi + private val incomingMessenger = Messenger(IncomingHandler()) + + @UnstableApi + inner class IncomingHandler() : Handler() { + override fun handleMessage(msg: Message) { + when (msg.what) { + SessionHandlerMessageTypes.TRIGGER_PLAYBACK -> handleTriggerPlayback(msg) + + SessionHandlerMessageTypes.GET_CONSUMPTION_REPORT -> handleGetConsumptionReport(msg) + + SessionHandlerMessageTypes.UPDATE_PLAYBACK_CONSUMPTION_REPORTING_CONFIGURATION -> handleUpdatePlaybackConsumptionReportingConfiguration( + msg + ) + + SessionHandlerMessageTypes.GET_QOE_METRICS_REPORT -> handleGetQoeMetricsReport(msg) + + else -> super.handleMessage(msg) + } + } + } + + @UnstableApi + private fun handleTriggerPlayback(msg: Message) { + val bundle: Bundle = msg.data + bundle.classLoader = PlaybackRequest::class.java.classLoader + val playbackRequest: PlaybackRequest? = bundle.getParcelable("playbackRequest") + if (playbackRequest != null) { + consumptionReportingController.handleTriggerPlayback(playbackRequest) + qoeMetricsReportingController.handleTriggerPlayback(playbackRequest) + sessionController.handleTriggerPlayback(playbackRequest) + } + } + + @UnstableApi + private fun handleGetConsumptionReport(msg: Message) { + consumptionReportingController.handleGetConsumptionReport(msg) + } + + @UnstableApi + private fun handleUpdatePlaybackConsumptionReportingConfiguration(msg: Message) { + consumptionReportingController.updateLastConsumptionRequest(msg) + } + + @UnstableApi + private fun handleGetQoeMetricsReport(msg: Message) { + qoeMetricsReportingController.handleGetQoeMetricsReport(msg) + } + + @UnstableApi + fun initialize( + consumptionReportingController: IConsumptionReportingController, + qoEMetricsReportingController: IQoEMetricsReportingController, + sessionController: ISessionController + ) { + this.consumptionReportingController = consumptionReportingController + this.qoeMetricsReportingController = qoEMetricsReportingController + this.sessionController = sessionController + } + + @UnstableApi + fun getIncomingMessenger(): Messenger { + return incomingMessenger + } + + + @UnstableApi + fun reset() { + consumptionReportingController.reset() + sessionController.reset() + qoeMetricsReportingController.reset() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/service/MessengerService.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/service/MessengerService.kt new file mode 100644 index 0000000..4411b3b --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/service/MessengerService.kt @@ -0,0 +1,95 @@ +package com.fivegmag.a5gmsmediastreamhandler.service + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.util.Log +import androidx.media3.common.util.UnstableApi +import com.fivegmag.a5gmscommonlibrary.models.ServiceListEntry + +class MessengerService( + private val context: Context +) { + + companion object { + const val TAG = "5GMS-MessengerService" + } + + private var boundToMediaSessionHandler = false + private lateinit var serviceConnectedCallbackFunction: () -> Unit + private lateinit var incomingMessageHandler: IncomingMessageHandler + private lateinit var outgoingMessageHandler: OutgoingMessageHandler + private val messengerConnection = object : ServiceConnection { + + @UnstableApi + override fun onServiceConnected(className: ComponentName, service: IBinder) { + boundToMediaSessionHandler = true + val nativeIncomingMessenger = incomingMessageHandler.getIncomingMessenger() + outgoingMessageHandler.handleServiceConnected(service, nativeIncomingMessenger) + serviceConnectedCallbackFunction() + } + + override fun onServiceDisconnected(className: ComponentName) { + boundToMediaSessionHandler = false + outgoingMessageHandler.handleServiceDisconnected() + } + } + + fun initialize( + incomingMessageHandler: IncomingMessageHandler, + outgoingMessageHandler: OutgoingMessageHandler + ) { + this.incomingMessageHandler = incomingMessageHandler + this.outgoingMessageHandler = outgoingMessageHandler + } + + + fun bind(onConnectionToMediaSessionHandlerEstablished: () -> (Unit)) { + try { + val intent = Intent() + intent.component = ComponentName( + "com.fivegmag.a5gmsmediasessionhandler", + "com.fivegmag.a5gmsmediasessionhandler.service.MediaSessionHandlerMessengerService" + ) + if (context.bindService(intent, messengerConnection, Context.BIND_AUTO_CREATE)) { + Log.i( + TAG, + "Binding to MediaSessionHandler service returned true" + ) + } else { + Log.i( + TAG, + "Binding to MediaSessionHandler service returned false" + ) + } + serviceConnectedCallbackFunction = onConnectionToMediaSessionHandlerEstablished + } catch (e: SecurityException) { + Log.e( + TAG, + "Can't bind to MediaSessionHandler, check permission in Manifest" + ) + } + } + + fun setM5Endpoint(m5BaseUrl: String) { + outgoingMessageHandler.setM5Endpoint(m5BaseUrl) + } + + fun initializePlaybackByServiceListEntry(serviceListEntry: ServiceListEntry) { + outgoingMessageHandler.initializePlaybackByServiceListEntry(serviceListEntry) + } + + @UnstableApi + fun reset() { + if (boundToMediaSessionHandler) { + context.unbindService(messengerConnection) + boundToMediaSessionHandler = false + } + outgoingMessageHandler.reset() + incomingMessageHandler.reset() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/service/OutgoingMessageHandler.kt b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/service/OutgoingMessageHandler.kt new file mode 100644 index 0000000..507349f --- /dev/null +++ b/app/src/main/java/com/fivegmag/a5gmsmediastreamhandler/service/OutgoingMessageHandler.kt @@ -0,0 +1,108 @@ +package com.fivegmag.a5gmsmediastreamhandler.service + +import android.os.Bundle +import android.os.IBinder +import android.os.Message +import android.os.Messenger +import android.os.RemoteException +import android.util.Log +import com.fivegmag.a5gmscommonlibrary.helpers.SessionHandlerMessageTypes +import com.fivegmag.a5gmscommonlibrary.models.ServiceListEntry + +class OutgoingMessageHandler { + + private var boundToMediaSessionHandler = false + private var nativeIncomingHandler: Messenger? = null + private var outgoingMessengerService: Messenger? = null + + companion object { + const val TAG = "5GMS-OutgoingMessageHandler" + } + + fun handleServiceConnected(service: IBinder, mHandler: Messenger) { + nativeIncomingHandler = mHandler + outgoingMessengerService = Messenger(service) + try { + Log.i(TAG, "Connected to Media Session Handler") + boundToMediaSessionHandler = true + registerClient() + } catch (_: RemoteException) { + } + + } + + fun handleServiceDisconnected() { + outgoingMessengerService = null + boundToMediaSessionHandler = false + } + + private fun registerClient() { + if (!canSendMessage()) { + return + } + val msg = getMessage(SessionHandlerMessageTypes.REGISTER_CLIENT) + sendMessage(msg) + } + + fun setM5Endpoint(m5BaseUrl: String) { + if (!canSendMessage()) { + return + } + val bundle = Bundle() + bundle.putString("m5BaseUrl", m5BaseUrl) + sendMessageByTypeAndBundle(SessionHandlerMessageTypes.SET_M5_ENDPOINT, bundle) + } + + fun initializePlaybackByServiceListEntry(serviceListEntry: ServiceListEntry) { + if (!canSendMessage()) { + return + } + val bundle = Bundle() + bundle.putParcelable("serviceListEntry", serviceListEntry) + sendMessageByTypeAndBundle(SessionHandlerMessageTypes.START_PLAYBACK_BY_SERVICE_LIST_ENTRY_MESSAGE, bundle) + } + + fun updatePlaybackState(state: String) { + if (!canSendMessage()) { + return + } + val bundle = Bundle() + bundle.putString("playbackState", state) + sendMessageByTypeAndBundle(SessionHandlerMessageTypes.STATUS_MESSAGE, bundle) + } + + fun sendMessageByTypeAndBundle(messageType: Int, bundle: Bundle) { + if (!canSendMessage()) { + return + } + val msg = getMessage(messageType) + msg.data = bundle + sendMessage(msg) + } + + private fun canSendMessage(): Boolean { + return boundToMediaSessionHandler + } + + private fun getMessage(messageType: Int): Message { + val msg: Message = Message.obtain( + null, + messageType + ) + msg.replyTo = nativeIncomingHandler + + return msg + } + + private fun sendMessage(msg: Message) { + try { + outgoingMessengerService?.send(msg) + } catch (e: RemoteException) { + e.printStackTrace() + } + } + + fun reset() { + handleServiceDisconnected() + } +} \ No newline at end of file