diff --git a/live-plot-graph/.gitignore b/live-plot-graph/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/live-plot-graph/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/live-plot-graph/build.gradle b/live-plot-graph/build.gradle
new file mode 100644
index 000000000..a4a00bd34
--- /dev/null
+++ b/live-plot-graph/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * ************************************************************************
+ * build.gradle
+ * *************************************************************************
+ * Copyright © 2020 VLC authors and VideoLAN
+ * Author: Nicolas POMEPUY
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ * **************************************************************************
+ *
+ *
+ */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles 'consumer-rules.pro'
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.core:core-ktx:1.1.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+ implementation project(':tools')
+ implementation "androidx.constraintlayout:constraintlayout:$rootProject.ext.constraintLayoutVersion"
+
+}
diff --git a/live-plot-graph/consumer-rules.pro b/live-plot-graph/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/live-plot-graph/proguard-rules.pro b/live-plot-graph/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/live-plot-graph/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/live-plot-graph/src/androidTest/java/org/videolan/liveplotgraph/ExampleInstrumentedTest.kt b/live-plot-graph/src/androidTest/java/org/videolan/liveplotgraph/ExampleInstrumentedTest.kt
new file mode 100644
index 000000000..bc1730904
--- /dev/null
+++ b/live-plot-graph/src/androidTest/java/org/videolan/liveplotgraph/ExampleInstrumentedTest.kt
@@ -0,0 +1,46 @@
+/*
+ * ************************************************************************
+ * ExampleInstrumentedTest.kt
+ * *************************************************************************
+ * Copyright © 2020 VLC authors and VideoLAN
+ * Author: Nicolas POMEPUY
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ * **************************************************************************
+ *
+ *
+ */
+
+package org.videolan.liveplotgraph
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("org.videolan.liveplotgraph.test", appContext.packageName)
+ }
+}
diff --git a/live-plot-graph/src/main/AndroidManifest.xml b/live-plot-graph/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..e1e8c0937
--- /dev/null
+++ b/live-plot-graph/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
diff --git a/live-plot-graph/src/main/java/org/videolan/liveplotgraph/LegendView.kt b/live-plot-graph/src/main/java/org/videolan/liveplotgraph/LegendView.kt
new file mode 100644
index 000000000..5f33beeaf
--- /dev/null
+++ b/live-plot-graph/src/main/java/org/videolan/liveplotgraph/LegendView.kt
@@ -0,0 +1,97 @@
+/*
+ * ************************************************************************
+ * LegendView.kt
+ * *************************************************************************
+ * Copyright © 2020 VLC authors and VideoLAN
+ * Author: Nicolas POMEPUY
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ * **************************************************************************
+ *
+ *
+ */
+
+package org.videolan.liveplotgraph
+
+import android.app.Activity
+import android.content.Context
+import android.util.AttributeSet
+import android.util.Log
+import android.view.ViewGroup
+import android.widget.GridLayout
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import org.videolan.tools.dp
+
+class LegendView : ConstraintLayout, PlotViewDataChangeListener {
+
+ private var plotViewId: Int = -1
+ private lateinit var plotView: PlotView
+
+ constructor(context: Context) : super(context) {
+ setWillNotDraw(false)
+ }
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
+ initAttributes(attrs, 0)
+ setWillNotDraw(false)
+ }
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
+ initAttributes(attrs, defStyle)
+ setWillNotDraw(false)
+ }
+
+ private fun initAttributes(attrs: AttributeSet, defStyle: Int) {
+ attrs.let {
+
+ val a = context.theme.obtainStyledAttributes(attrs, R.styleable.LPGPlotView, 0, defStyle)
+ try {
+ plotViewId = a.getResourceId(a.getIndex(R.styleable.LPGLegendView_lpg_plot_view), -1)
+ } catch (e: Exception) {
+ Log.w("", e.message, e)
+ } finally {
+ a.recycle()
+ }
+ }
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ plotView = (context as Activity).findViewById(plotViewId)
+ if (!::plotView.isInitialized) throw IllegalStateException("A valid plot view has to be provided")
+ plotView.addListener(this)
+ layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
+ }
+
+ override fun onDataChanged(data: List>) {
+ removeAllViews()
+ val grid = GridLayout(context)
+ grid.columnCount = 2
+ addView(grid)
+ data.forEach {
+ val title = TextView(context)
+ title.text = it.first.title
+ title.setTextColor(it.first.color)
+ grid.addView(title)
+
+ val value = TextView(context)
+ value.text = it.second
+ val layoutParams = GridLayout.LayoutParams()
+ layoutParams.leftMargin = 4.dp
+ value.layoutParams = layoutParams
+ grid.addView(value)
+ }
+ }
+}
\ No newline at end of file
diff --git a/live-plot-graph/src/main/java/org/videolan/liveplotgraph/LineGraph.kt b/live-plot-graph/src/main/java/org/videolan/liveplotgraph/LineGraph.kt
new file mode 100644
index 000000000..c3c140f3b
--- /dev/null
+++ b/live-plot-graph/src/main/java/org/videolan/liveplotgraph/LineGraph.kt
@@ -0,0 +1,44 @@
+/*
+ * ************************************************************************
+ * LineGraph.kt
+ * *************************************************************************
+ * Copyright © 2020 VLC authors and VideoLAN
+ * Author: Nicolas POMEPUY
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ * **************************************************************************
+ *
+ *
+ */
+
+package org.videolan.liveplotgraph
+
+import android.graphics.Paint
+import org.videolan.tools.dp
+
+data class LineGraph(val index: Int, val title: String, val color: Int, val data: HashMap = HashMap()) {
+ val paint: Paint by lazy {
+ val p = Paint()
+ p.color = color
+ p.strokeWidth = 2.dp.toFloat()
+ p.isAntiAlias = true
+ p.style = Paint.Style.STROKE
+ p
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other is LineGraph && other.index == index) return true
+ return super.equals(other)
+ }
+}
\ No newline at end of file
diff --git a/live-plot-graph/src/main/java/org/videolan/liveplotgraph/PlotView.kt b/live-plot-graph/src/main/java/org/videolan/liveplotgraph/PlotView.kt
new file mode 100644
index 000000000..2ee1f3473
--- /dev/null
+++ b/live-plot-graph/src/main/java/org/videolan/liveplotgraph/PlotView.kt
@@ -0,0 +1,253 @@
+/*
+ * ************************************************************************
+ * PlotView.kt
+ * *************************************************************************
+ * Copyright © 2020 VLC authors and VideoLAN
+ * Author: Nicolas POMEPUY
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ * **************************************************************************
+ *
+ *
+ */
+
+package org.videolan.liveplotgraph
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.util.Log
+import android.widget.FrameLayout
+import org.videolan.tools.dp
+import kotlin.math.log10
+import kotlin.math.pow
+import kotlin.math.round
+
+class PlotView : FrameLayout {
+ private val textPaint: Paint by lazy {
+ val p = Paint()
+ p.color = color
+ p.textSize = 10.dp.toFloat()
+ p
+ }
+ val data = ArrayList()
+ private val maxsY = ArrayList()
+ private val maxsX = ArrayList()
+ private val minsX = ArrayList()
+ private var color: Int = 0xFFFFFF
+ private var listeners = ArrayList()
+
+ constructor(context: Context) : super(context) {
+ setWillNotDraw(false)
+ }
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
+ initAttributes(attrs, 0)
+ setWillNotDraw(false)
+ }
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
+ initAttributes(attrs, defStyle)
+ setWillNotDraw(false)
+ }
+
+ private fun initAttributes(attrs: AttributeSet, defStyle: Int) {
+ attrs.let {
+ val a = context.theme.obtainStyledAttributes(attrs, R.styleable.LPGPlotView, 0, defStyle)
+ try {
+ color = a.getInt(R.styleable.LPGPlotView_lpg_color, 0xFFFFFF)
+ } catch (e: Exception) {
+ Log.w("", e.message, e)
+ } finally {
+ a.recycle()
+ }
+ }
+ }
+
+ fun addData(index: Int, value: Pair) {
+ data.forEach { lineGraph ->
+ if (lineGraph.index == index) {
+ lineGraph.data[value.first] = value.second
+ if (lineGraph.data.size > 30) {
+ lineGraph.data.remove(lineGraph.data.toSortedMap().firstKey())
+ }
+ invalidate()
+ val listenerValue = ArrayList>(data.size)
+ data.forEach { lineGraph ->
+ listenerValue.add(Pair(lineGraph, "${String.format("%.0f", lineGraph.data[lineGraph.data.keys.max()])} kb/s"))
+ }
+ listeners.forEach { it.onDataChanged(listenerValue) }
+ }
+ }
+ }
+
+ fun addListener(listener: PlotViewDataChangeListener) {
+ listeners.add(listener)
+ }
+
+ fun removeListener(listener: PlotViewDataChangeListener) {
+ listeners.remove(listener)
+ }
+
+ override fun onDraw(canvas: Canvas?) {
+ super.onDraw(canvas)
+
+ maxsY.clear()
+ maxsX.clear()
+ minsX.clear()
+
+ data.forEach {
+ maxsY.add(it.data.maxBy { it.value }?.value ?: 0f)
+ }
+ val maxY = maxsY.max() ?: 0f
+
+ data.forEach {
+ maxsX.add(it.data.maxBy { it.key }?.key ?: 0L)
+ }
+ val maxX = maxsX.max() ?: 0L
+
+ data.forEach {
+ minsX.add(it.data.minBy { it.key }?.key ?: 0L)
+ }
+ val minX = minsX.min() ?: 0L
+
+ drawLines(maxY, minX, maxX, canvas)
+ drawGrid(canvas, maxY, minX, maxX)
+ }
+
+ private fun drawGrid(canvas: Canvas?, maxY: Float, minX: Long, maxX: Long) {
+ canvas?.let {
+ if (maxY <= 0F) return
+ // it.drawText("0", 10F, it.height.toFloat() - 2.dp, textPaint)
+ it.drawText("${String.format("%.0f", maxY)} kb/s", 10F, 10.dp.toFloat(), textPaint)
+
+ var center = maxY / 2
+ center = getRoundedByUnit(center)
+ if (BuildConfig.DEBUG) Log.d(this::class.java.simpleName, "Center: $center")
+ val centerCoord = measuredHeight * ((maxY - center) / maxY)
+ it.drawLine(0f, centerCoord, measuredWidth.toFloat(), centerCoord, textPaint)
+ it.drawText("${String.format("%.0f", center)} kb/s", 10F, centerCoord - 2.dp, textPaint)
+
+ //timestamps
+
+ var index = maxX - 1000
+ if (BuildConfig.DEBUG) Log.d(this::class.java.simpleName, "FirstIndex: $index")
+ while (index > minX) {
+ val xCoord = (measuredWidth * ((index - minX).toDouble() / (maxX - minX).toDouble())).toFloat()
+ it.drawLine(xCoord, 0F, xCoord, measuredHeight.toFloat() - 12.dp, textPaint)
+ val formattedText = "${String.format("%.0f", getRoundedByUnit((index - maxX).toFloat()) / 1000)}s"
+ it.drawText(formattedText, xCoord - (textPaint.measureText(formattedText) / 2), measuredHeight.toFloat(), textPaint)
+ index -= 1000
+ }
+
+ }
+ }
+
+ private fun getRoundedByUnit(number: Float): Float {
+ val lengthX = log10(number.toDouble()).toInt()
+ return (round(number / (10.0.pow(lengthX.toDouble()))) * (10.0.pow(lengthX.toDouble()))).toFloat()
+ }
+
+ private fun drawLines(maxY: Float, minX: Long, maxX: Long, canvas: Canvas?) {
+ data.forEach { line ->
+ var initialPoint: Pair? = null
+ line.data.toSortedMap().forEach { point ->
+ if (initialPoint == null) {
+ initialPoint = getCoordinates(point, maxY, minX, maxX, measuredWidth, measuredHeight)
+ } else {
+ val currentPoint = getCoordinates(point, maxY, minX, maxX, measuredWidth, measuredHeight)
+ currentPoint.let {
+ canvas?.drawLine(initialPoint!!.first, initialPoint!!.second, it.first, it.second, line.paint)
+ initialPoint = it
+ }
+ }
+ }
+ }
+ }
+
+// fun drawLines2(maxY: Float, minX: Long, maxX: Long, canvas: Canvas?) {
+//
+//
+// data.forEach { line ->
+// path.reset()
+// val points = line.data.map {
+// val coord = getCoordinates(it, maxY, minX, maxX, measuredWidth, measuredHeight)
+// GraphPoint(coord.first, coord.second)
+// }.sortedBy { it.x }
+// for (i in points.indices) {
+// val point = points[i]
+// val smoothing = 100
+// when (i) {
+// 0 -> {
+// val next: GraphPoint = points[i + 1]
+// point.dx = (next.x - point.x) / smoothing
+// point.dy = (next.y - point.y) / smoothing
+// }
+// points.size - 1 -> {
+// val prev: GraphPoint = points[i - 1]
+// point.dx = (point.x - prev.x) / smoothing
+// point.dy = (point.y - prev.y) / smoothing
+// }
+// else -> {
+// val next: GraphPoint = points[i + 1]
+// val prev: GraphPoint = points[i - 1]
+// point.dx = next.x - prev.x / smoothing
+// point.dy = (next.y - prev.y) / smoothing
+// }
+// }
+// }
+// for (i in points.indices) {
+// val point: GraphPoint = points[i]
+// when {
+// i == 0 -> {
+// path.moveTo(point.x, point.y)
+// }
+// i < points.size - 1 -> {
+// val prev: GraphPoint = points[i - 1]
+// path.cubicTo(prev.x + prev.dx, prev.y + prev.dy, point.x - point.dx, point.y - point.dy, point.x, point.y)
+// canvas?.drawCircle(point.x, point.y, 2.dp.toFloat(), line.paint)
+// }
+// else -> {
+// path.lineTo(point.x, point.y)
+// }
+// }
+// }
+// canvas?.drawPath(path, line.paint)
+// }
+//
+// }
+
+ private fun getCoordinates(point: Map.Entry, maxY: Float, minX: Long, maxX: Long, measuredWidth: Int, measuredHeight: Int): Pair = Pair((measuredWidth * ((point.key - minX).toDouble() / (maxX - minX).toDouble())).toFloat(), measuredHeight * ((maxY - point.value) / maxY))
+ fun clear() {
+ data.forEach {
+ it.data.clear()
+ }
+ }
+
+ fun addLine(lineGraph: LineGraph) {
+ if (!data.contains(lineGraph)) {
+ data.add(lineGraph)
+ }
+ }
+}
+
+data class GraphPoint(val x: Float, val y: Float) {
+ var dx: Float = 0F
+ var dy: Float = 0F
+}
+
+interface PlotViewDataChangeListener {
+ fun onDataChanged(data: List>)
+}
\ No newline at end of file
diff --git a/live-plot-graph/src/main/res/values/attrs.xml b/live-plot-graph/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..fdc737414
--- /dev/null
+++ b/live-plot-graph/src/main/res/values/attrs.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/live-plot-graph/src/main/res/values/strings.xml b/live-plot-graph/src/main/res/values/strings.xml
new file mode 100644
index 000000000..5588f673d
--- /dev/null
+++ b/live-plot-graph/src/main/res/values/strings.xml
@@ -0,0 +1,27 @@
+
+
+
+ live-plot-graph
+
diff --git a/live-plot-graph/src/test/java/org/videolan/liveplotgraph/ExampleUnitTest.kt b/live-plot-graph/src/test/java/org/videolan/liveplotgraph/ExampleUnitTest.kt
new file mode 100644
index 000000000..e82e4e5b4
--- /dev/null
+++ b/live-plot-graph/src/test/java/org/videolan/liveplotgraph/ExampleUnitTest.kt
@@ -0,0 +1,41 @@
+/*
+ * ************************************************************************
+ * ExampleUnitTest.kt
+ * *************************************************************************
+ * Copyright © 2020 VLC authors and VideoLAN
+ * Author: Nicolas POMEPUY
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ * **************************************************************************
+ *
+ *
+ */
+
+package org.videolan.liveplotgraph
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml
index f68a97a15..10ffedccd 100644
--- a/resources/src/main/res/values/strings.xml
+++ b/resources/src/main/res/values/strings.xml
@@ -780,4 +780,7 @@
You will lose your progresses and the playlists you created.\n%s
Set start point
Set end point
+ Demux Bitrate
+ Input bitrate
+ Video stats
diff --git a/settings.gradle b/settings.gradle
index 0d5c4203e..c655a5883 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,3 @@
-include ':libvlc', ':api', ':medialibrary', ':tools', ':resources', ':mediadb'
+include ':libvlc', ':api', ':medialibrary', ':tools', ':resources', ':mediadb', ':live-plot-graph'
include ':vlc-android'
include ':moviepedia'
diff --git a/vlc-android/build.gradle b/vlc-android/build.gradle
index 529c78e4f..032a077e9 100644
--- a/vlc-android/build.gradle
+++ b/vlc-android/build.gradle
@@ -253,6 +253,7 @@ dependencies {
implementation project(':tools')
implementation project(':resources')
implementation project(':mediadb')
+ implementation project(':live-plot-graph')
// AppCompat
implementation "androidx.activity:activity-ktx:$rootProject.ext.androidxActivityVersion"
diff --git a/vlc-android/res/drawable/ic_video_stats.xml b/vlc-android/res/drawable/ic_video_stats.xml
new file mode 100644
index 000000000..edb8b2aad
--- /dev/null
+++ b/vlc-android/res/drawable/ic_video_stats.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
diff --git a/vlc-android/res/layout/player_hud.xml b/vlc-android/res/layout/player_hud.xml
index ca189ab10..20b998332 100644
--- a/vlc-android/res/layout/player_hud.xml
+++ b/vlc-android/res/layout/player_hud.xml
@@ -35,13 +35,45 @@
tools:theme="@style/Theme.VLC.TV"
tools:visibility="visible">
+
+
+
+
+
+
+
+
+ vlc:layout_constraintStart_toStartOf="@+id/constraintLayout2"
+ vlc:layout_constraintTop_toBottomOf="@id/stats_container" />
+
diff --git a/vlc-android/res/values/colors.xml b/vlc-android/res/values/colors.xml
index fa86d6156..28cee46e0 100644
--- a/vlc-android/res/values/colors.xml
+++ b/vlc-android/res/values/colors.xml
@@ -74,4 +74,6 @@
#1c313a
#455a64
+ #2196f3
+ #e91e63
\ No newline at end of file
diff --git a/vlc-android/res/values/styles.xml b/vlc-android/res/values/styles.xml
index a86fb3244..180c45399 100644
--- a/vlc-android/res/values/styles.xml
+++ b/vlc-android/res/values/styles.xml
@@ -92,6 +92,7 @@
- @drawable/ic_passthrough
- @drawable/ic_abrepeat
- @drawable/ic_abrepeat_reset
+ - @drawable/ic_video_stats
- @drawable/ic_dial
- @color/grey400
- @color/grey50
@@ -210,6 +211,7 @@
- @drawable/ic_passthrough_w
- @drawable/ic_abrepeat
- @drawable/ic_abrepeat_reset
+ - @drawable/ic_video_stats
- @drawable/ic_dial_w
- @color/orange500
- @color/grey700
@@ -398,6 +400,7 @@
- @color/grey200
- @drawable/ic_abrepeat
- @drawable/ic_abrepeat_reset
+ - @drawable/ic_video_stats
- @drawable/ic_dial_w
- @drawable/ic_crop_player
- @color/orange500
diff --git a/vlc-android/src/org/videolan/vlc/gui/helpers/PlayerOptionsDelegate.kt b/vlc-android/src/org/videolan/vlc/gui/helpers/PlayerOptionsDelegate.kt
index e4170070a..ac89b4078 100644
--- a/vlc-android/src/org/videolan/vlc/gui/helpers/PlayerOptionsDelegate.kt
+++ b/vlc-android/src/org/videolan/vlc/gui/helpers/PlayerOptionsDelegate.kt
@@ -59,6 +59,7 @@ private const val ID_SHUFFLE = 11
private const val ID_PASSTHROUGH = 12
private const val ID_ABREPEAT = 13
private const val ID_OVERLAY_SIZE = 14
+private const val ID_VIDEO_STATS = 15
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
@@ -116,6 +117,7 @@ class PlayerOptionsDelegate(val activity: AppCompatActivity, val service: Playba
if (service.canShuffle()) options.add(PlayerOption(playerOptionType, ID_SHUFFLE, R.drawable.ic_shuffle, res.getString(R.string.shuffle_title)))
val chaptersCount = service.getChapters(-1)?.size ?: 0
if (chaptersCount > 1) options.add(PlayerOption(playerOptionType, ID_CHAPTER_TITLE, R.attr.ic_chapter_normal_style, res.getString(R.string.go_to_chapter)))
+ options.add(PlayerOption(playerOptionType, ID_VIDEO_STATS, R.attr.ic_video_stats, res.getString(R.string.video_stats)))
}
options.add(PlayerOption(playerOptionType, ID_ABREPEAT, R.attr.ic_abrepeat, res.getString(R.string.ab_repeat)))
options.add(PlayerOption(playerOptionType, ID_SAVE_PLAYLIST, R.attr.ic_save, res.getString(R.string.playlist_save)))
@@ -195,6 +197,10 @@ class PlayerOptionsDelegate(val activity: AppCompatActivity, val service: Playba
hide()
service.playlistManager.toggleABRepeat()
}
+ ID_VIDEO_STATS -> {
+ hide()
+ service.playlistManager.toggleStats()
+ }
else -> showFragment(option.id)
}
}
diff --git a/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt b/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt
index 23009677a..c76c7791b 100644
--- a/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt
+++ b/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt
@@ -193,6 +193,7 @@ open class VideoPlayerActivity : AppCompatActivity(), IPlaybackSettingsControlle
internal var fov: Float = 0.toFloat()
private var touchDelegate: VideoTouchDelegate? = null
+ private var statsDelegate: VideoStatsDelegate? = null
private var isTv: Boolean = false
// Tracks & Subtitles
@@ -496,6 +497,7 @@ open class VideoPlayerActivity : AppCompatActivity(), IPlaybackSettingsControlle
val xRange = Math.max(dm.widthPixels, dm.heightPixels)
val sc = ScreenConfig(dm, xRange, yRange, currentScreenOrientation)
touchDelegate = VideoTouchDelegate(this, touch, sc, isTv)
+ statsDelegate = VideoStatsDelegate(this)
UiTools.setRotationAnimation(this)
if (savedInstanceState != null) {
savedTime = savedInstanceState.getLong(KEY_TIME)
@@ -736,6 +738,7 @@ open class VideoPlayerActivity : AppCompatActivity(), IPlaybackSettingsControlle
previousMediaPath = null
addedExternalSubs.clear()
medialibrary.resumeBackgroundOperations()
+ statsDelegate?.stop()
}
private fun saveBrightness() {
@@ -2135,6 +2138,13 @@ open class VideoPlayerActivity : AppCompatActivity(), IPlaybackSettingsControlle
manageAbRepeatStep()
})
+ service.playlistManager.videoStatsOn.observe(this, Observer {
+ if (it) showOverlay(true)
+ statsDelegate?.container = hudBinding.statsContainer
+ statsDelegate?.initPlotView(hudBinding.plotView)
+ if (it) statsDelegate?.start() else statsDelegate?.stop()
+ })
+
hudBinding.lifecycleOwner = this
val layoutParams = hudBinding.progressOverlay.layoutParams as RelativeLayout.LayoutParams
if (AndroidDevices.isPhone || !AndroidDevices.hasNavBar)
diff --git a/vlc-android/src/org/videolan/vlc/gui/video/VideoStatsDelegate.kt b/vlc-android/src/org/videolan/vlc/gui/video/VideoStatsDelegate.kt
new file mode 100644
index 000000000..f8423dadf
--- /dev/null
+++ b/vlc-android/src/org/videolan/vlc/gui/video/VideoStatsDelegate.kt
@@ -0,0 +1,98 @@
+/*
+ * ************************************************************************
+ * VideoStatsDelegate.kt
+ * *************************************************************************
+ * Copyright © 2020 VLC authors and VideoLAN
+ * Author: Nicolas POMEPUY
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ * **************************************************************************
+ *
+ *
+ */
+
+package org.videolan.vlc.gui.video
+
+import android.annotation.SuppressLint
+import android.os.Handler
+import android.util.Log
+import android.view.View
+import android.widget.GridLayout
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.ObsoleteCoroutinesApi
+import org.videolan.libvlc.Media
+import org.videolan.liveplotgraph.LineGraph
+import org.videolan.liveplotgraph.PlotView
+import org.videolan.vlc.BuildConfig
+import org.videolan.vlc.R
+
+@ExperimentalCoroutinesApi
+@ObsoleteCoroutinesApi
+class VideoStatsDelegate(private val player: VideoPlayerActivity) {
+ lateinit var container: ConstraintLayout
+ private var started = false
+ private val plotHandler: Handler = Handler()
+ private val firstTimecode = System.currentTimeMillis()
+ lateinit var plotView: PlotView
+
+ fun stop() {
+ started = false
+ plotHandler.removeCallbacks(runnable)
+ container.visibility = View.GONE
+ plotView.clear()
+ }
+
+ fun start() {
+ started = true
+ plotHandler.postDelayed(runnable, 300)
+ container.visibility = View.VISIBLE
+ }
+
+ fun initPlotView(plotView: PlotView) {
+ this.plotView = plotView
+ plotView.addLine(LineGraph(StatIndex.DEMUX_BITRATE.ordinal, player.getString(R.string.demux_bitrate), ContextCompat.getColor(player, R.color.material_blue)))
+ plotView.addLine(LineGraph(StatIndex.INPUT_BITRATE.ordinal, player.getString(R.string.input_bitrate), ContextCompat.getColor(player, R.color.material_pink)))
+ }
+
+ @SuppressLint("SetTextI18n")
+ private val runnable = Runnable {
+ val media = player.service?.mediaplayer?.media as? Media ?: return@Runnable
+
+ if (BuildConfig.DEBUG) Log.i(this::class.java.simpleName, "Stats: demuxBitrate: ${media.stats?.demuxBitrate} demuxCorrupted: ${media.stats?.demuxCorrupted} demuxDiscontinuity: ${media.stats?.demuxDiscontinuity} demuxReadBytes: ${media.stats?.demuxReadBytes}")
+ val now = System.currentTimeMillis() - firstTimecode
+ media.stats?.demuxBitrate?.let {
+ plotView.addData(StatIndex.DEMUX_BITRATE.ordinal, Pair(now, it * 8 * 1024))
+ }
+ media.stats?.inputBitrate?.let {
+ plotView.addData(StatIndex.INPUT_BITRATE.ordinal, Pair(now, it * 8 * 1024))
+ }
+
+ media.let {
+ for (i in 0 until it.trackCount) {
+ val grid = GridLayout(player)
+ grid.columnCount = 2
+ }
+ }
+
+ if (started) {
+ start()
+ }
+ }
+}
+
+enum class StatIndex {
+ INPUT_BITRATE, DEMUX_BITRATE
+}
\ No newline at end of file
diff --git a/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt b/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt
index fb610ba49..4dbaf0716 100644
--- a/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt
+++ b/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt
@@ -71,6 +71,7 @@ class PlaylistManager(val service: PlaybackService) : MediaWrapperList.EventList
private var entryUrl : String? = null
val abRepeat by lazy(LazyThreadSafetyMode.NONE) { MutableLiveData().apply { value = ABRepeat() } }
val abRepeatOn by lazy(LazyThreadSafetyMode.NONE) { MutableLiveData().apply { value = false } }
+ val videoStatsOn by lazy(LazyThreadSafetyMode.NONE) { MutableLiveData().apply { value = false } }
private val mediaFactory = FactoryManager.getFactory(IMediaFactory.factoryId) as IMediaFactory
@@ -720,6 +721,10 @@ class PlaylistManager(val service: PlaybackService) : MediaWrapperList.EventList
}
}
+ fun toggleStats() {
+ videoStatsOn.value = !videoStatsOn.value!!
+ }
+
fun clearABRepeat() {
abRepeat.value = abRepeat.value?.apply {
start = -1L