Skip to content

Open Audio-Video Telemetry for Android. Multiplatform tracking for audio and video players with events and metrics monitoring.

License

Notifications You must be signed in to change notification settings

asllop/OpenAVT-Android

Repository files navigation

OpenAVT-Android

License

  1. Introduction
  2. Installation
  3. Usage
  4. Custom Elements
  5. Examples
  6. Documentation
  7. Author
  8. License

1. Introduction

The Open Audio-Video Telemetry is a set of tools for performance monitoring in multimedia applications. The objectives are similar to those of the OpenTelemetry project, but specifically for sensing data from audio and video players. OpenAVT can be configured to generate Events, Metrics, or a combination of both.

2. Installation

To install OpenAVT-Android using JitPack, add the following lines to your root build.gradle:

allprojects {
    repositories {
        ...
        
        // Add this line at the end of your repositories
        maven { url 'https://jitpack.io' }
    }
}

And then add in your app build.gradle one line per each module:

2.1 Core

This one is mandatory, needed by the rest of modules.

dependencies {
    ...
    implementation 'com.github.asllop.OpenAVT-Android:OpenAVT-Core:master-SNAPSHOT'
}

2.2 ExoPlayer Tracker

dependencies {
    ...
    implementation 'com.github.asllop.OpenAVT-Android:OpenAVT-ExoPlayer:master-SNAPSHOT'
    
    // ExoPlayer is a dependency of OpenAVT-ExoPlayer
    implementation 'com.google.android.exoplayer:exoplayer:+'
}

2.3 Google IMA Tracker

dependencies {
    ...
    implementation 'com.github.asllop.OpenAVT-Android:OpenAVT-IMA:master-SNAPSHOT'
    
    // ExoPlayer IMA extension is a dependency of OpenAVT-IMA
    implementation 'com.google.android.exoplayer:extension-ima:+'
}

2.4 InfluxDB Backend

dependencies {
    ...
    implementation 'com.github.asllop.OpenAVT-Android:OpenAVT-InfluxDB:master-SNAPSHOT'
}

2.5 Graphite Backend

dependencies {
    ...
    implementation 'com.github.asllop.OpenAVT-Android:OpenAVT-Graphite:master-SNAPSHOT'
}

2.6 New Relic Backend

dependencies {
    ...
    implementation 'com.github.asllop.OpenAVT-Android:OpenAVT-NewRelic:master-SNAPSHOT'
}

3. Usage

There are many ways to use the OpenAVT library, depending on the use case, here we will cover the most common combinations. We won't explain all the possible arguments passed to the constructors, only the essential ones. For the rest check out the documentation.

3.1 Choosing a Backend

The first step is choosing the backend where the data will be sent.

3.1.1 Init the InfluxDB Backend

val backend = OAVTBackendInfluxdb(url = URL("http://192.168.99.100:8086/write?db=test"))

url is the URL of the InfluxDB server used to write data to a particular database (in this case named test).

3.1.2 Init the Graphite Backend

val backend = OAVTBackendGraphite(host = "192.168.99.100")

host is the address of the Graphite server.

3.1.3 Init the New Relic Backend

val backend = OAVTBackendNewrelic()

The New Relic Mobile Agent must be installed and set up to use this backend.

3.2 Choosing a Hub

Next, we will choose a Hub. This element is used to obtain the data coming from the trackers and process it to pass the proper events to the backend. Users can implement their logic for this and use their custom hubs, but OpenAVT provides a default implementation that works for most cases.

For instruments with video tracker only, we will choose:

val hub = OAVTHubCore()

And for instruments with video and ads tracker:

val hub = OAVTHubCoreAds()

3.3 Choosing a Metricalc

This step is optional and only necessary if we want to generate metrics, if we only need events this section can be omitted. A Metricalc is something like a Hub but for metrics, it gets events and processes them to generate metrics. Again, users can provide custom implementation, but the OpenAVT library provides a default one:

val metricalc = OAVTMetricalcCore()

3.4 Choosing Trackers

And finally, the trackers, the piece that generates the data. Currently, OpenAVT provides two trackers: ExoPlayer and Google IMA Ads. We won't cover how to set up the ExoPlayer and IMA libraries, for this check out the corresponding documentation or the examples.

3.4.1 Init the ExoPlayer Tracker

val tracker = OAVTTrackerExoPlayer(player)

Where player is an instance of the SimpleExoPlayer.

3.4.2 Init the IMA Tracker

val adTracker = OAVTTrackerIMA()

3.5 Creating the Instrument

Once we have all the elements, the only step left is putting everything together:

val instrument = OAVTInstrument(hub = hub, metricalc = metricalc, backend = backend)
val trackerId = instrument.addTracker(tracker)
val adTrackerId = instrument.addTracker(adTracker)
instrument.ready()

Here we have created a new instrument that contains all the elements, and once all are present, we called ready() to initialize everything, This will cause the execution of the method OAVTComponentInterface.instrumentReady(...) in all trackers, hub, metricalc and backend. Now the instrument is ready to start generating data.

4. Custom Elements

OpenAVT provides a set of elements that cover a wide range of possibilities, but not all. For this reason, the most interesting capability it offers is its flexibility to accept custom implementations of these elements.

4.1 Custom Actions

Actions are instances of the class OAVTAction, and generatic a custom action is as easy as creating a new instance, providing the action name in the constructor:

val myAction = OAVTAction("CustomAction")

By convention, action names are in upper camel case.

Now we can use it normally as any other action, for examplem, on an emit:

instrument.emit(myAction, trackerId)

4.2 Custom Attributes

Attributes are instances of the class OAVTAttribute. We build a custom attribute by creating a new instance of the class, providing the name in the constructor:

val myAttr = OAVTAttribute("customAttribute")

By convention, attribute names are in lower camel case.

In the previous section we saw how to create custom actions, but we left something. All actions have an associated time-since attribute. The attribute name is autogenerated based on the action name, being timeSince plus the action name the default. In the case of the example, it will be timeSinceCustomAction. But we can provide an attribute in the constructor if the default one doesn't work for us:

val myAction = OAVTAction("CustomAction", OAVTAttribute("myTimeSince"))

You can read more on time-since attributes here, section 4.3 Attributes.

A custom attribute can be used as any other attribute, for example, setting it on an event:

// `event` is an instance of OAVTEvent
event.attributes[myAttr] = "any value"

4.3 Custom Metrics

Metrics are instances of the class OAVTMetric, and we build custom metrics by creating new instances of the class, providing the metric name, type and value in the constructor:

val myMetric = OAVTMetric("CustomMetric", OAVTMetric.MetricType.Gauge, 10.1)

By convention, metric names are in upper camel case, like action names.

4.2 Custom Components

Components are objects that are part of an instrument, and conform to one of the derived interfaces of OAVTComponentInterface. In OpenAVT there are four types of components: Trackers, Hubs, Metricalcs and Backends.

Instruments allow hot-plugging of components, by using the lifecycle methods defined in the OAVTComponentInterface. With OAVTInstrument.addTracker(...) and OAVTInstrument.removeTracker(...) we can add and remove tracker, and with OAVTInstrument.setHub(...), OAVTInstrument.setMetrical(...) and OAVTInstrument.setBackend(...) we can set and overwrite hubs, metricals and backends. When this happens, the instrument calls OAVTComponentInterface.endOfService() on the removed component. After any change on the instrument is made, we must call OAVTInstrument.ready(), that will call OAVTComponentInterface.ready() on each component.

4.2.1 Custom Trackers

A tracker is the element that knows about specific players, reading properties, registering observers, etc. In OpenAVT a tracker is a class that conforms to the OAVTTrackerInterface, that in turn extends the OAVTComponentInterface. So, the simplest possible tracker will look like:

class DummyTracker: OAVTTrackerInterface {
    override fun initEvent(event: OAVTEvent): OAVTEvent? {
        // Called when an emit(...) happens. It receives the event and must return an event or null.
        // If an event is returned, it will be passed to the Hub.
        return event
    }

    // Tracker ID, set by the instrument when the tracker is created.
    override var trackerId: Int? = null
    // Tracker state.
    override var state: OAVTState = OAVTState()

    override fun instrumentReady(instrument: OAVTInstrument) {
        // Called when ready() is called on the instrument.
    }

    override fun endOfService() {
        // Called when the tracker is removed from the instrument or when shutdown() is called.
    }
}

This tracker does almost nothing, just bypass the events received. But we could improve it a bit, let's say we want to send an event when the instrument is ready:

    private var instrument: OAVTInstrument? = null
    ...

    override fun instrumentReady(instrument: OAVTInstrument) {
        if (this.instrument == null) {
            this.instrument = instrument
            this.instrument?.emit(OAVTAction.TrackerInit, this)
        }
    }

And now maybe we want to set a custom attribute to that event, but only that, no other one:

    override fun initEvent(event: OAVTEvent): OAVTEvent? {
        if (event.action == OAVTAction.TrackerInit) {
            event.attributes[OAVTAttribute("myCustomAttr")] = 1000
        }
        return event
    }

Any event generated calling emit will pass thought this method (if the tracker argument of emit points to this tracker). Most events will be generated from within the tracker, when something happens in the player (a stream starts, the user pauses the playback, etc), but we can also call emit from any other place.

Generally, instrumentReady is used to do initializations, like registering observers in the player, set up states, send starting events, etc. And endOfService is used to undo all this, unregister observers, etc.

A tracker can also register attribute getters. An attribute getter binds a tracker method with an OAVTAttribute. Let's say our player reports the current playback position, and we want to include this attribute on every event. OpenAVT offers a pre-defined attribute to report this information: OAVTAttribute.position. We can define a method in our tracker that returns that position:

    fun getPosition(): Int? {
        val p = ... //do whatever with the supported player to get the position.
        return p
    }

By convention, times are reported as integers in milliseconds.

Now we need to bind this method to the attribute:

    fun instrumentReady(instrument: OAVTInstrument) {
        ...
        
        this.instrument?.registerGetter(OAVTAttribute.position, ::getPosition, this)
    }

After this, every event that is emitted will contain the attribute OAVTAttribute.position automatically. But what if we only want to include the attribute in some events? For this we have an optional argument in the registerGetter, the filter. We pass it a function that returns a boolean:

    this.instrument?.registerGetter(OAVTAttribute.position, ::getPosition, this, {event, _ ->
        event.action == OAVTAction.Ping
    })

Now only ping events will have the attribute.

But, why all this complexity? It would be much easier to just call the getPosition method, and then set the attribute using OAVTEvent.attributes.

Certainly we could do that and it would work. But by registering attribute getters, any element outside the tracker, for example a Hub, can query for a specific attribute value (using OAVTInstrument.callGetter(...)), doesn't matter the class and the interface. And if the queried getter is not defined, it will just return nil.

4.2.2 Custom Hubs

A hub is the element that contains the bussiness logic. It receives events from the tracker and according to the type, state, and other conditions, it decides what to do. It can also act over other components, for example updating trackers state. In OpenAVT a hub is a class that conforms to the OAVTHubInterface, that in turn extends the OAVTComponentInterface. A simple hub could look like:

class DummyHub: OAVTHubInterface {
    override fun processEvent(event: OAVTEvent, tracker: OAVTTrackerInterface): OAVTEvent? {
        // Called with the result of tracker's initEvent. It receives the event and must return an event or nil.
        // If an event is returned, it will be sent to the Metricalc and the Backend.
        return event
    }

    override fun instrumentReady(instrument: OAVTInstrument) {
        // Called when ready() is called on the instrument.
    }

    override fun endOfService() {
        // Called when the tracker is removed from the instrument or when shutdown() is called.
    }
}

The main method for a hub is the processEvent, that is called with the event returned by a tracker. Along with the event, it receives the tracker that generated it.

This simple hub does nothing more than bypassing the events received, but it could implement complex logics: It could update the tracker's state depending on the received events, block an event that is not supposed to happen, add or modify attributes, start or stop timers, etc. It's up to your particular use case. In the following example we see how to handle the pause logic:

    override fun processEvent(event: OAVTEvent, tracker: OAVTTrackerInterface): OAVTEvent? {
        if (event.action == OAVTAction.PauseBegin) {
            if (tracker.state.isPaused) {
                return null
            }
            tracker.state.isPaused = true
        }
        else if (event.action == OAVTAction.PauseFinish) {
            if (!tracker.state.isPaused) {
                return null
            }
            tracker.state.isPaused = false
        }
        return event
    }

4.2.3 Custom Metricalcs

A metricalc is similar to a hub, but for metrics, it handles the business logic to generate metrics. A metricalc is a class that conforms to the OAVTMetricalcInterface:

class DummyMetricalc: OAVTMetricalcInterface {
    override fun processMetric(event: OAVTEvent, tracker: OAVTTrackerInterface): Array<OAVTMetric> {
        // Called with the result of hub's processEvent. It receives the event and returns an array of metrics.
        // If any metric is returned, it will be sent to the Backend.
        return arrayOf()
    }

    override fun instrumentReady(instrument: OAVTInstrument) {
        // Called when ready() is called on the instrument.
    }

    override fun endOfService() {
        // Called when the tracker is removed from the instrument or when shutdown() is called.
    }
}

This metricalc does nothing, it generates no metrics. Let's imagine we want to generate a metric that measures the time between quality change events. We could do something like:

    private var tsOfLastEvent: Long = 0L
    
    override fun processMetric(event: OAVTEvent, tracker: OAVTTrackerInterface): Array<OAVTMetric> {
        if (event.action == OAVTAction.QualityChangeUp || event.action == OAVTAction.QualityChangeDown) {
            if (this.tsOfLastEvent > 0) {
                val metric = OAVTMetric("TimeBetweenQualityChanges", OAVTMetric.MetricType.Gauge, System.currentTimeMillis() - this.tsOfLastEvent)
                this.tsOfLastEvent = System.currentTimeMillis()
                return arrayOf(metric)
            }
        }
        return arrayOf()
    }

4.2.4 Custom Backends

The final stop for an event is the backend, that is a class conforming to the OAVTBackendInterface. Is the backend's duty to store or redirect data to a database, server, filesystem, etc.

class DummyBackend: OAVTBackendInterface {
    override fun sendEvent(event: OAVTEvent) {
        // Called with the result of hub's processEvent.
    }

    override fun sendMetric(metric: OAVTMetric) {
        // Called with the results of metricalc's processMetric.
    }

    override fun instrumentReady(instrument: OAVTInstrument) {
        // Called when ready() is called on the instrument.
    }

    override fun endOfService() {
        // Called when the tracker is removed from the instrument or when shutdown() is called.
    }
}

The method sendMetric is called once with each metric returned by metricalc's processMetric.

5. Examples

Checkout the app folder for usage examples.

6. Documentation

Check out the Documentation Repository for general and platform-independent documentation.

All classes and methods are documented with annotations. To generate the docs you can use Dokka. Just open the project in Android Studio, go to the Gradle menu, and double-click on OpenAVT-Android > Tasks > documentation > dokka.

7. Author

Andreu Santarén Llop (asllop)
andreu.santaren at gmail .com

8. License

OpenAVT-Android is available under the MIT license. See the LICENSE file for more info.

About

Open Audio-Video Telemetry for Android. Multiplatform tracking for audio and video players with events and metrics monitoring.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages