From 080da5890783fc031adf81578336b7f72171c7c8 Mon Sep 17 00:00:00 2001 From: Nicolas Pomepuy Date: Mon, 13 Jan 2020 10:09:55 +0100 Subject: [PATCH] New module for video stats --- live-plot-graph/.gitignore | 1 + live-plot-graph/build.gradle | 62 +++++ live-plot-graph/consumer-rules.pro | 0 live-plot-graph/proguard-rules.pro | 21 ++ .../liveplotgraph/ExampleInstrumentedTest.kt | 46 ++++ live-plot-graph/src/main/AndroidManifest.xml | 26 ++ .../org/videolan/liveplotgraph/LegendView.kt | 97 +++++++ .../org/videolan/liveplotgraph/LineGraph.kt | 44 +++ .../org/videolan/liveplotgraph/PlotView.kt | 253 ++++++++++++++++++ live-plot-graph/src/main/res/values/attrs.xml | 33 +++ .../src/main/res/values/strings.xml | 27 ++ .../videolan/liveplotgraph/ExampleUnitTest.kt | 41 +++ resources/src/main/res/values/strings.xml | 3 + settings.gradle | 2 +- vlc-android/build.gradle | 1 + vlc-android/res/drawable/ic_video_stats.xml | 33 +++ vlc-android/res/layout/player_hud.xml | 34 ++- vlc-android/res/values/attrs.xml | 1 + vlc-android/res/values/colors.xml | 2 + vlc-android/res/values/styles.xml | 3 + .../vlc/gui/helpers/PlayerOptionsDelegate.kt | 6 + .../vlc/gui/video/VideoPlayerActivity.kt | 10 + .../vlc/gui/video/VideoStatsDelegate.kt | 98 +++++++ .../org/videolan/vlc/media/PlaylistManager.kt | 5 + 24 files changed, 847 insertions(+), 2 deletions(-) create mode 100644 live-plot-graph/.gitignore create mode 100644 live-plot-graph/build.gradle create mode 100644 live-plot-graph/consumer-rules.pro create mode 100644 live-plot-graph/proguard-rules.pro create mode 100644 live-plot-graph/src/androidTest/java/org/videolan/liveplotgraph/ExampleInstrumentedTest.kt create mode 100644 live-plot-graph/src/main/AndroidManifest.xml create mode 100644 live-plot-graph/src/main/java/org/videolan/liveplotgraph/LegendView.kt create mode 100644 live-plot-graph/src/main/java/org/videolan/liveplotgraph/LineGraph.kt create mode 100644 live-plot-graph/src/main/java/org/videolan/liveplotgraph/PlotView.kt create mode 100644 live-plot-graph/src/main/res/values/attrs.xml create mode 100644 live-plot-graph/src/main/res/values/strings.xml create mode 100644 live-plot-graph/src/test/java/org/videolan/liveplotgraph/ExampleUnitTest.kt create mode 100644 vlc-android/res/drawable/ic_video_stats.xml create mode 100644 vlc-android/src/org/videolan/vlc/gui/video/VideoStatsDelegate.kt 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