The Open Audio-Video Telemetry is a set of tools for performance monitoring in multimedia applications. The Open Audio-Video Telemetry is a set of tools for performance monitoring in multimedia applications. This repo contains the platform specific components to monitor Android and AndroidTV video and audio applications.
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:
This one is mandatory, needed by the rest of modules.
dependencies {
...
implementation 'com.github.asllop.OpenAVT-Android:OpenAVT-Core:master-SNAPSHOT'
}
dependencies {
...
implementation 'com.github.asllop.OpenAVT-Android:OpenAVT-ExoPlayer:master-SNAPSHOT'
// ExoPlayer is a dependency of OpenAVT-ExoPlayer
implementation 'com.google.android.exoplayer:exoplayer:+'
}
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:+'
}
dependencies {
...
implementation 'com.github.asllop.OpenAVT-Android:OpenAVT-InfluxDB:master-SNAPSHOT'
}
dependencies {
...
implementation 'com.github.asllop.OpenAVT-Android:OpenAVT-Graphite:master-SNAPSHOT'
}
dependencies {
...
implementation 'com.github.asllop.OpenAVT-Android:OpenAVT-NewRelic:master-SNAPSHOT'
}
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.
The first step is choosing the backend where the data will be sent.
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
).
val backend = OAVTBackendGraphite(host = "192.168.99.100")
host
is the address of the Graphite server.
val backend = OAVTBackendNewrelic()
The New Relic Mobile Agent must be installed and set up to use this backend.
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()
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()
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.
val tracker = OAVTTrackerExoPlayer(player)
Where player
is an instance of the SimpleExoPlayer.
val adTracker = OAVTTrackerIMA()
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.
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.
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)
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"
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.
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.
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.
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
}
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()
}
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
.
Checkout the app
folder for usage examples.
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
.
Andreu Santarén Llop (asllop)
andreu.santaren at gmail .com
OpenAVT-Android is available under the MIT license. See the LICENSE file for more info.