Alpha version

This commit is contained in:
Anthony Chomienne 2021-06-22 16:05:02 +02:00
commit 6eb9be5fe5
348 changed files with 40275 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx

65
.idea/$CACHE_FILE$ Normal file
View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">
<profile-state>
<expanded-state>
<State>
<id />
</State>
<State>
<id>AccessibilityLintAndroid</id>
</State>
<State>
<id>Android</id>
</State>
<State>
<id>Bidirectional TextInternationalizationLintAndroid</id>
</State>
<State>
<id>Chrome OSCorrectnessLintAndroid</id>
</State>
<State>
<id>Class structureJava</id>
</State>
<State>
<id>CorrectnessLintAndroid</id>
</State>
<State>
<id>IconsUsabilityLintAndroid</id>
</State>
<State>
<id>InternationalizationLintAndroid</id>
</State>
<State>
<id>Java</id>
</State>
<State>
<id>Javadoc issuesJava</id>
</State>
<State>
<id>LintAndroid</id>
</State>
<State>
<id>LintLintAndroid</id>
</State>
<State>
<id>MessagesCorrectnessLintAndroid</id>
</State>
<State>
<id>PerformanceLintAndroid</id>
</State>
<State>
<id>SecurityLintAndroid</id>
</State>
<State>
<id>TypographyUsabilityLintAndroid</id>
</State>
<State>
<id>UsabilityLintAndroid</id>
</State>
</expanded-state>
</profile-state>
</entry>
</component>
</project>

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
Peeriscope

View File

@ -0,0 +1,139 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="150" />
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
.idea/compiler.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

View File

@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="schoumi">
<words>
<w>datas</w>
<w>rtmp</w>
</words>
</dictionary>
</component>

27
.idea/gradle.xml Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="$APPLICATION_HOME_DIR$/gradle/gradle-2.2.1" />
<option name="gradleJvm" value="1.8" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/encoder" />
<option value="$PROJECT_DIR$/rtmp" />
<option value="$PROJECT_DIR$/rtplibrary" />
<option value="$PROJECT_DIR$/rtsp" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</component>
</project>

30
.idea/jarRepositories.xml Normal file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
</component>
</project>

14
.idea/misc.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CMakeSettings">
<configurations>
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
</configurations>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

59
app/build.gradle Normal file
View File

@ -0,0 +1,59 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'androidx.navigation.safeargs.kotlin'
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "fr.mobdev.peertubelive"
minSdkVersion 19
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables
{
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
dataBinding true
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation project(':rtplibrary')
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -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

BIN
app/release/app-release.apk Normal file

Binary file not shown.

View File

@ -0,0 +1,18 @@
{
"version": 2,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "org.framasoft.peertubelive",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
]
}

Binary file not shown.

View File

@ -0,0 +1,24 @@
package org.framasoft.peertubelive
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* 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.framasoft.peeriscope", appContext.packageName)
}
}

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fr.mobdev.peertubelive">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="fr.mobdev.peertubelive.activity.MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="fr.mobdev.peertubelive.activity.StreamActivity"
android:theme="@style/Theme.Design.Light.NoActionBar" android:screenOrientation="portrait"/>
<activity android:name="fr.mobdev.peertubelive.activity.CreateLiveActivity" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

View File

@ -0,0 +1,58 @@
package fr.mobdev.peertubelive.activity
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import fr.mobdev.peertubelive.R
import fr.mobdev.peertubelive.databinding.InstanceItemBinding
import fr.mobdev.peertubelive.objects.OAuthData
class AccountAdapter(private var accounts: List<OAuthData>): RecyclerView.Adapter<AccountAdapter.ViewHolder>() {
var onDeleteAccount: OnDeleteAccount? = null
var onClickListener: OnClickListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = DataBindingUtil.inflate<InstanceItemBinding>(LayoutInflater.from(parent.context), R.layout.instance_item, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val oauthData = accounts[position]
holder.binding.username.text = oauthData.username
holder.binding.url.text = oauthData.baseUrl
holder.pos = position
}
override fun getItemCount(): Int {
return accounts.size
}
fun setAccounts(accounts: List<OAuthData>) {
this.accounts = accounts
notifyDataSetChanged()
}
inner class ViewHolder(val binding: InstanceItemBinding) : RecyclerView.ViewHolder(binding.root) {
var pos: Int = 0
init {
binding.root.setOnClickListener {
onClickListener?.onClick(accounts[pos])
}
binding.root.setOnLongClickListener {
onDeleteAccount?.onDeleteAccount(accounts[pos])
return@setOnLongClickListener true
}
}
}
interface OnDeleteAccount{
fun onDeleteAccount(oAuthData: OAuthData)
}
interface OnClickListener{
fun onClick(oAuthData: OAuthData)
}
}

View File

@ -0,0 +1,373 @@
package fr.mobdev.peertubelive.activity
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import fr.mobdev.peertubelive.R
import fr.mobdev.peertubelive.databinding.ChannelListBinding
import fr.mobdev.peertubelive.manager.DatabaseManager
import fr.mobdev.peertubelive.manager.InstanceManager
import fr.mobdev.peertubelive.objects.ChannelData
import fr.mobdev.peertubelive.objects.OAuthData
import fr.mobdev.peertubelive.objects.StreamData
import fr.mobdev.peertubelive.objects.StreamSettings
import fr.mobdev.peertubelive.utils.TranslationUtils
import java.util.ArrayList
class CreateLiveActivity : AppCompatActivity() {
companion object{
const val OAUTH_DATA = "OAUTH_DATA"
}
private lateinit var channels: List<ChannelData>
private lateinit var categories: MutableMap<String, Int>
private lateinit var licences: MutableMap<String, Int>
private lateinit var privacies: MutableMap<String, Int>
private lateinit var languages: MutableMap<String, String>
private lateinit var oAuthData: OAuthData
private var inError: Boolean = false
private var showAdvancedSettings = true
private lateinit var binding: ChannelListBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
oAuthData = intent.getParcelableExtra(OAUTH_DATA)!!
binding = DataBindingUtil.setContentView(this,R.layout.channel_list)
binding.error.visibility = View.GONE
binding.channelList.visibility = View.GONE
binding.liveTitle.visibility = View.GONE
binding.channel.visibility = View.GONE
binding.title.visibility = View.GONE
binding.titleError.visibility = View.GONE
binding.advanceSettings.visibility = View.GONE
binding.privacy.visibility = View.GONE
binding.privacyList.visibility = View.GONE
toggleAdvanceSettings()
binding.goLive.isEnabled = false
binding.advanceSettings.setOnClickListener{
toggleAdvanceSettings()
}
binding.goLive.setOnClickListener {
val title = binding.liveTitle.text.toString()
val channel = channels[binding.channelList.selectedItemPosition].id
var category: Int? = categories[binding.categoryList.selectedItem.toString()]!!
val privacy = privacies[binding.privacyList.selectedItem.toString()]!!
if (category == 0)
category = null
var language: String? = languages[binding.languageList.selectedItem.toString()]!!
if (language != null && language.isEmpty())
language = null
var licence: Int? = licences[binding.licenceList.selectedItem.toString()]!!
if (licence == 0)
licence = null
var description: String? = binding.description.text.toString()
if (description != null && description.isEmpty())
description = null
val comments = binding.commentsEnabled.isChecked
val download = binding.downloadEnabled.isChecked
val nsfw = binding.nsfw.isChecked
val replay = binding.saveReplay.isChecked
val streamSettings = StreamSettings(title,channel,privacy,category,language,licence,description,comments,download,nsfw,replay)
DatabaseManager.updateStreamSettings(this,streamSettings)
if(title.isEmpty())
{
binding.titleError.visibility = View.VISIBLE
} else {
binding.titleError.visibility = View.GONE
goLive(streamSettings)
}
}
InstanceManager.getCategoryList(this,oAuthData.baseUrl!!,object : InstanceManager.InstanceListener {
override fun onSuccess(args: Bundle?) {
val map = args?.getSerializable(InstanceManager.EXTRA_DATA)!! as Map<String, Int>
categories = HashMap(map.count())
for(pair in map.entries) {
val stringId = TranslationUtils.getCategoryTranslationFor(pair.key)
if(stringId != -1)
categories[getString(stringId)] = pair.value
else
categories[pair.key] = pair.value
}
updateView(null)
}
override fun onError(error: String?) {
inError = true
updateView(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
this@CreateLiveActivity.oAuthData.updateData(oauthData)
}
})
InstanceManager.getUserChannelList(this,oAuthData.baseUrl!!,oAuthData, object : InstanceManager.InstanceListener {
override fun onSuccess(args: Bundle?) {
channels = args?.getParcelableArrayList<ChannelData>(InstanceManager.EXTRA_DATA)!!
updateView(null)
}
override fun onError(error: String?) {
inError = true
updateView(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
this@CreateLiveActivity.oAuthData.updateData(oauthData)
}
})
InstanceManager.getPrivacyList(this,oAuthData.baseUrl!!, object : InstanceManager.InstanceListener {
override fun onSuccess(args: Bundle?) {
val map = args?.getSerializable(InstanceManager.EXTRA_DATA)!! as Map<String, Int>
privacies = HashMap(map.count())
for(pair in map.entries) {
val stringId = TranslationUtils.getPrivacyTranslationFor(pair.key)
if(stringId != -1)
privacies[getString(stringId)] = pair.value
else
privacies[pair.key] = pair.value
}
updateView(null)
}
override fun onError(error: String?) {
inError = true
updateView(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
this@CreateLiveActivity.oAuthData.updateData(oauthData)
}
})
InstanceManager.getLicencesList(this,oAuthData.baseUrl!!, object : InstanceManager.InstanceListener {
override fun onSuccess(args: Bundle?) {
var map = args?.getSerializable(InstanceManager.EXTRA_DATA)!! as Map<String, Int>
licences = HashMap(map.count())
for(pair in map.entries) {
val stringId = TranslationUtils.getLicenceTranslationFor(pair.key)
if(stringId != -1)
licences[getString(stringId)] = pair.value
else
licences[pair.key] = pair.value
}
updateView(null)
}
override fun onError(error: String?) {
inError = true
updateView(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
this@CreateLiveActivity.oAuthData.updateData(oauthData)
}
})
InstanceManager.getLanguageList(this,oAuthData.baseUrl!!, object : InstanceManager.InstanceListener {
override fun onSuccess(args: Bundle?) {
val map = args?.getSerializable(InstanceManager.EXTRA_DATA)!! as Map<String, String>
languages = HashMap(map.count())
for(pair in map.entries) {
val stringId = TranslationUtils.getLanguageTranslationFor(pair.key)
if(stringId != -1)
languages[getString(stringId)] = pair.value
else
languages[pair.key] = pair.value
}
updateView(null)
}
override fun onError(error: String?) {
inError = true
updateView(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
this@CreateLiveActivity.oAuthData.updateData(oauthData)
}
})
}
private fun updateView(error: String?) {
if(!inError) {
if(this::channels.isInitialized && this::categories.isInitialized && this::privacies.isInitialized && this::languages.isInitialized && this::licences.isInitialized) {
val channelAdapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item)
for (channel in channels) {
channelAdapter.add(channel.name)
}
val categoryAdapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item)
val categoryList = ArrayList(categories.keys)
categoryList.sort()
categoryAdapter.addAll(categoryList)
val licencesAdapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item)
licencesAdapter.addAll(licences.keys)
licencesAdapter.sort { o1, o2 -> licences[o1]!!.compareTo(licences[o2]!!)}
val privacyAdapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item)
privacyAdapter.addAll(privacies.keys)
privacyAdapter.sort { o1, o2 -> privacies[o1]!!.compareTo(privacies[o2]!!)}
val languageAdapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item)
val languageList = ArrayList(languages.keys)
languageList.sort()
languageAdapter.addAll(languageList)
runOnUiThread {
binding.goLive.isEnabled = true
binding.channelList.adapter = channelAdapter
binding.categoryList.adapter = categoryAdapter
binding.privacyList.adapter = privacyAdapter
binding.licenceList.adapter = licencesAdapter
binding.languageList.adapter = languageAdapter
binding.loadingProgress.visibility = View.GONE
binding.loadingChannels.visibility = View.GONE
binding.channelList.visibility = View.VISIBLE
binding.liveTitle.visibility = View.VISIBLE
binding.channel.visibility = View.VISIBLE
binding.title.visibility = View.VISIBLE
binding.advanceSettings.visibility = View.VISIBLE
binding.privacy.visibility = View.VISIBLE
binding.privacyList.visibility = View.VISIBLE
restoreSettings()
}
}
} else {
runOnUiThread {
binding.error.text = error
binding.error.visibility = View.VISIBLE
binding.loadingProgress.visibility = View.GONE
binding.loadingChannels.visibility = View.GONE
}
}
}
private fun goLive(streamSettings: StreamSettings) {
InstanceManager.createLive(this,oAuthData.baseUrl!!,oAuthData,streamSettings,object : InstanceManager.InstanceListener {
override fun onSuccess(args: Bundle?) {
if(args != null) {
val streamData = args.getParcelable<StreamData>(InstanceManager.EXTRA_DATA)!!
val intent = Intent(this@CreateLiveActivity, StreamActivity::class.java)
intent.putExtra(InstanceManager.EXTRA_DATA,streamData)
startActivityForResult(intent,2)
}
}
override fun onError(error: String?) {
runOnUiThread {
binding.error.text = error
binding.error.visibility = View.VISIBLE
}
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
}
})
}
private fun toggleAdvanceSettings() {
showAdvancedSettings = !showAdvancedSettings
var status = View.VISIBLE
binding.advanceSettings.setText(R.string.advanced_settings_expand)
if (!showAdvancedSettings) {
status = View.GONE
binding.advanceSettings.setText(R.string.advanced_settings)
}
binding.category.visibility = status
binding.categoryList.visibility = status
binding.licence.visibility = status
binding.licenceList.visibility = status
binding.language.visibility = status
binding.languageList.visibility = status
binding.description.visibility = status
binding.descriptionTitle.visibility = status
binding.commentsEnabled.visibility = status
binding.commentsEnabledTitle.visibility = status
binding.downloadEnabled.visibility = status
binding.downloadEnabledTitle.visibility = status
binding.nsfw.visibility = status
binding.nsfwTitle.visibility = status
binding.saveReplay.visibility = status
binding.saveReplayTitle.visibility = status
binding.saveReplayInfo.visibility = status
}
private fun restoreSettings() {
val settings = DatabaseManager.getStreamSettings(this)
if (settings != null) {
binding.commentsEnabled.isChecked = settings.comments
binding.downloadEnabled.isChecked = settings.download
binding.nsfw.isChecked = settings.nsfw
binding.saveReplay.isChecked = settings.saveReplay
binding.liveTitle.setText(settings.title)
if (settings.privacy != 0) {
for (privacyIdx in 0..privacies.count()) {
val privacy = binding.privacyList.getItemAtPosition(privacyIdx)
if (privacies[privacy] == settings.privacy) {
binding.privacyList.setSelection(privacyIdx)
break
}
}
}
binding.privacyList.setSelection(settings.privacy -1)
if (settings.licence != 0 && settings.licence != null) {
for (licenceIdx in 0..licences.count()) {
val licence = binding.licenceList.getItemAtPosition(licenceIdx)
if (licences[licence] == settings.licence) {
binding.licenceList.setSelection(licenceIdx)
break
}
}
}
if(settings.category != 0) {
for (categoryIdx in 0..categories.count()) {
val category = binding.categoryList.getItemAtPosition(categoryIdx)
if (categories[category] == settings.category) {
binding.categoryList.setSelection(categoryIdx)
break
}
}
}
if (settings.language != null) {
for (languageIdx in 0..languages.count()) {
val language = binding.languageList.getItemAtPosition(languageIdx)
if (languages[language].equals(settings.language)) {
binding.languageList.setSelection(languageIdx)
break
}
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
setResult(resultCode)
finish()
}
}

View File

@ -0,0 +1,143 @@
package fr.mobdev.peertubelive.activity
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.LinearLayoutManager
import fr.mobdev.peertubelive.R
import fr.mobdev.peertubelive.databinding.HomeBinding
import fr.mobdev.peertubelive.dialog.AddInstanceDialog
import fr.mobdev.peertubelive.manager.DatabaseManager
import fr.mobdev.peertubelive.objects.OAuthData
import java.util.*
class MainActivity : AppCompatActivity() {
private lateinit var accounts : List<OAuthData>
private lateinit var adapter: AccountAdapter
private lateinit var binding: HomeBinding
override fun onCreate(savedInstanceState: Bundle?) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.home)
binding.instanceList.layoutManager = LinearLayoutManager(binding.root.context)
setupList()
}
override fun onResume() {
super.onResume()
setupList()
}
private fun setupList() {
accounts = DatabaseManager.getCredentials(binding.root.context)
if (accounts.isNotEmpty()) {
if (!this::adapter.isInitialized) {
adapter = AccountAdapter(accounts)
adapter.onDeleteAccount = object: AccountAdapter.OnDeleteAccount {
override fun onDeleteAccount(oAuthData: OAuthData) {
val builder = AlertDialog.Builder(this@MainActivity)
builder.setMessage(getString(R.string.delete_account, oAuthData.username, oAuthData.baseUrl))
builder.setTitle(R.string.delete_account_title)
builder.setPositiveButton(R.string.yes){ _: DialogInterface, _: Int ->
DatabaseManager.deleteAccount(this@MainActivity, oAuthData)
setupList()
}
builder.setNegativeButton(R.string.no){ _: DialogInterface, _: Int ->
//Do Nothing
}
builder.show()
}
};
adapter.onClickListener = object: AccountAdapter.OnClickListener {
override fun onClick(oAuthData: OAuthData) {
if (oAuthData.refreshTokenExpires < Calendar.getInstance().timeInMillis) {
addOrUpdateAccount(oAuthData)
} else {
goLive(oAuthData)
}
}
}
binding.instanceList.adapter = adapter
} else {
adapter.setAccounts(accounts)
}
binding.noInstance.visibility = View.GONE
binding.instanceList.visibility = View.VISIBLE
}
else {
binding.noInstance.visibility = View.VISIBLE
binding.instanceList.visibility = View.GONE
}
}
private fun goLive(oAuthData: OAuthData) {
val intent = Intent(this, CreateLiveActivity::class.java)
intent.putExtra(CreateLiveActivity.OAUTH_DATA, oAuthData)
startActivityForResult(intent,1)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.home, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if(item.itemId == R.id.add_instance) {
addOrUpdateAccount(null)
return true
}
return false
}
private fun addOrUpdateAccount(shouldUpdateOAuthData: OAuthData?) {
val wizard = AddInstanceDialog()
if(shouldUpdateOAuthData != null)
wizard.setOauthData(shouldUpdateOAuthData)
wizard.setOnAddInstanceListener(object : AddInstanceDialog.OnAddInstanceListener {
override fun addSuccess(oAuthData: OAuthData) {
runOnUiThread {
setupList()
if (shouldUpdateOAuthData != null) {
goLive(oAuthData)
}
}
}
})
wizard.show(supportFragmentManager, "Wizard")
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val alertBuilder = AlertDialog.Builder(this)
alertBuilder.setTitle(R.string.stream_ended)
when (resultCode) {
StreamActivity.BACKGROUND -> {
alertBuilder.setMessage(R.string.background_reason)
}
StreamActivity.BACK -> {
alertBuilder.setMessage(R.string.back_reason)
}
StreamActivity.LOCK -> {
alertBuilder.setMessage(R.string.lock_reason)
}
}
alertBuilder.setPositiveButton(android.R.string.ok,null)
alertBuilder.show()
}
}

View File

@ -0,0 +1,336 @@
package fr.mobdev.peertubelive.activity
import android.Manifest.permission
import android.content.*
import android.content.pm.PackageManager
import android.hardware.SensorManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.OrientationEventListener
import android.view.SurfaceHolder
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import com.pedro.encoder.input.video.CameraHelper
import com.pedro.rtplibrary.rtmp.RtmpCamera1
import net.ossrs.rtmp.ConnectCheckerRtmp
import fr.mobdev.peertubelive.R
import fr.mobdev.peertubelive.databinding.StreamBinding
import fr.mobdev.peertubelive.manager.InstanceManager.EXTRA_DATA
import fr.mobdev.peertubelive.objects.StreamData
class StreamActivity : AppCompatActivity() {
private lateinit var binding: StreamBinding
private lateinit var rtmpCamera1 : RtmpCamera1
private lateinit var streamData: StreamData
private lateinit var orientationEventListener: OrientationEventListener
private lateinit var lockReceiver: BroadcastReceiver
private var surfaceInit: Boolean = false
private var permissionGiven: Boolean = false
private var streamIsActive: Boolean = false
private var screenOrientation: Int = 0
private var lastScreenOrientation: Int = 0
private var orientationCounter: Int = 0
private var rotationIsLanternEnabled: Boolean = true
companion object {
const val BACKGROUND :Int = 1
const val LOCK :Int = 2
const val BACK :Int = 3
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding = DataBindingUtil.setContentView(this, R.layout.stream)
orientationEventListener = object: OrientationEventListener(this, SensorManager.SENSOR_DELAY_NORMAL){
override fun onOrientationChanged(orientation: Int) {
if(orientation < 0 || !rotationIsLanternEnabled)
return
var localOrientation: Int
localOrientation = when (orientation) {
in 45..135 -> {
90
}
in 135..225 -> {
180
}
in 225..315 -> {
270
}
else -> {
0
}
}
if(localOrientation != lastScreenOrientation) {
lastScreenOrientation = localOrientation
orientationCounter = 0
} else {
orientationCounter++
}
if (lastScreenOrientation != screenOrientation && orientationCounter > 30) {
screenOrientation = lastScreenOrientation
rtmpCamera1.glInterface.setStreamRotation(screenOrientation)
if (screenOrientation == 90) {
localOrientation = 270
} else if(screenOrientation == 270) {
localOrientation = 90
}
binding.flash.rotation = localOrientation.toFloat()
binding.muteMicro.rotation = localOrientation.toFloat()
binding.switchCamera.rotation = localOrientation.toFloat()
}
}
}
orientationEventListener.enable()
streamData = intent.getParcelableExtra<StreamData>(EXTRA_DATA)!!
binding.switchCamera.setOnClickListener { rtmpCamera1.switchCamera() }
binding.muteMicro.setOnClickListener {
if (rtmpCamera1.isAudioMuted) {
rtmpCamera1.enableAudio()
binding.muteMicro.setImageResource(R.drawable.baseline_volume_up_24)
}
else {
rtmpCamera1.disableAudio()
binding.muteMicro.setImageResource(R.drawable.baseline_volume_off_24)
}
}
binding.flash.setOnClickListener {
if (rtmpCamera1.isLanternEnabled) {
rtmpCamera1.disableLantern()
binding.flash.setImageResource(R.drawable.baseline_flash_off_24)
}
else {
rtmpCamera1.enableLantern()
binding.flash.setImageResource(R.drawable.baseline_flash_on_24)
}
}
binding.rotation.setOnClickListener {
if (rotationIsLanternEnabled) {
rotationIsLanternEnabled = !rotationIsLanternEnabled
binding.rotation.setImageResource(R.drawable.baseline_screen_lock_rotation_24)
}
else {
rotationIsLanternEnabled = !rotationIsLanternEnabled
binding.rotation.setImageResource(R.drawable.baseline_screen_rotation_24)
}
}
binding.surfaceView.holder.addCallback(object: SurfaceHolder.Callback {
override fun surfaceCreated(p0: SurfaceHolder) {
surfaceInit = true
if (permissionGiven)
startStream()
}
override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {
}
override fun surfaceDestroyed(p0: SurfaceHolder) {
if(rtmpCamera1.isStreaming) {
rtmpCamera1.stopStream()
}
rtmpCamera1.stopPreview()
}
}
)
lockReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action.equals(Intent.ACTION_SCREEN_OFF)){
setResult(LOCK)
finish()
} else if (intent?.action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)){
val reason = intent?.getStringExtra("reason")
if(reason.equals("homekey")){
setResult(BACKGROUND)
finish()
}
}
}
}
val filter = IntentFilter(Intent.ACTION_SCREEN_OFF)
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
registerReceiver(lockReceiver, filter)
}
override fun onResume() {
super.onResume()
val permissions = getUnAllowedPermissions()
if(permissions.isNotEmpty()) {
var shouldShowRequest = true
for(perm in permissions){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
shouldShowRequest = shouldShowRequest && shouldShowRequestPermissionRationale(perm)
}
if(shouldShowRequest) {
binding.permissionInfo.visibility = View.VISIBLE
binding.gotoPermission.visibility = View.VISIBLE
binding.surfaceView.visibility = View.GONE
binding.gotoPermission.setOnClickListener {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", packageName, null)
startActivity(intent)
}
} else {
ActivityCompat.requestPermissions(this, permissions.toTypedArray(), 1)
}
} else {
permissionGiven = true
if(surfaceInit && !streamIsActive)
startStream()
}
}
override fun onStop() {
super.onStop()
if (!hasWindowFocus()) {
unregisterReceiver(lockReceiver)
setResult(BACKGROUND)
finish()
}
}
override fun onBackPressed() {
val alertBuilder = AlertDialog.Builder(this)
alertBuilder.setTitle(R.string.end_stream)
alertBuilder.setMessage(R.string.ask_end_stream)
alertBuilder.setPositiveButton(R.string.yes) { _: DialogInterface, _: Int ->
setResult(BACK)
finish()
}
alertBuilder.setNegativeButton(R.string.no,null)
alertBuilder.show()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
var allPermissionGranted = true
for (result in grantResults) {
allPermissionGranted = allPermissionGranted && (result == PackageManager.PERMISSION_GRANTED)
}
if (allPermissionGranted) {
permissionGiven = true
if(surfaceInit && !streamIsActive)
startStream()
} else {
binding.permissionInfo.visibility = View.VISIBLE
binding.gotoPermission.visibility = View.VISIBLE
binding.surfaceView.visibility = View.GONE
}
}
private fun startStream() {
streamIsActive = true
binding.permissionInfo.visibility = View.GONE
binding.gotoPermission.visibility = View.GONE
binding.surfaceView.visibility = View.VISIBLE
val connectChecker : ConnectCheckerRtmp = object : ConnectCheckerRtmp {
override fun onConnectionSuccessRtmp() {
runOnUiThread {
Toast.makeText(binding.root.context, "Connection success", Toast.LENGTH_SHORT).show();
}
}
override fun onConnectionFailedRtmp(reason: String) {
runOnUiThread {
Toast.makeText(binding.root.context, "Connection failed", Toast.LENGTH_SHORT).show();
rtmpCamera1.stopStream()
}
}
override fun onNewBitrateRtmp(bitrate: Long) {
}
override fun onDisconnectRtmp() {
runOnUiThread {
Toast.makeText(binding.root.context, "Disconnect", Toast.LENGTH_SHORT).show();
}
}
override fun onAuthErrorRtmp() {
runOnUiThread {
Toast.makeText(binding.root.context, "Auth Error", Toast.LENGTH_SHORT).show();
}
}
override fun onAuthSuccessRtmp() {
runOnUiThread {
Toast.makeText(binding.root.context, "Auth Success", Toast.LENGTH_SHORT).show();
}
}
}
rtmpCamera1 = RtmpCamera1(binding.surfaceView, connectChecker)
var resolutions = rtmpCamera1.resolutionsBack
var width: Int
var height: Int
if(resolutions[0].width > resolutions[0].height) {
width = resolutions[0].width
height = resolutions[0].height
} else {
width = resolutions[0].height
height = resolutions[0].width
}
for(res in resolutions) {
if (width * height < res.width * res.width) {
if(res.width > res.height) {
width = res.width
height = res.height
} else {
width = res.height
height = res.width
}
}
}
rtmpCamera1.startPreview(width,height)
//start stream
if (rtmpCamera1.prepareAudio() && rtmpCamera1.prepareVideo(width,height,30,3000*1024,false,CameraHelper.getCameraOrientation(this))) {
rtmpCamera1.startStream(streamData.url+"/"+streamData.key)
} else {
/**This device cant init encoders, this could be for 2 reasons: The encoder selected doesnt support any configuration setted or your device hasnt a H264 or AAC encoder (in this case you can see log error valid encoder not found) */
}
}
private fun getUnAllowedPermissions(): List<String> {
val permissions: ArrayList<String> = ArrayList<String>()
if (ContextCompat.checkSelfPermission(this, permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
permissions.add(permission.CAMERA)
}
if (ContextCompat.checkSelfPermission(this, permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
permissions.add(permission.RECORD_AUDIO)
}
return permissions
}
}

View File

@ -0,0 +1,168 @@
package fr.mobdev.peertubelive.dialog
import android.app.Dialog
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.DialogFragment
import fr.mobdev.peertubelive.R
import fr.mobdev.peertubelive.databinding.AddInstanceBinding
import fr.mobdev.peertubelive.manager.DatabaseManager
import fr.mobdev.peertubelive.manager.InstanceManager
import fr.mobdev.peertubelive.objects.OAuthData
import java.net.MalformedURLException
import java.net.URL
class AddInstanceDialog : DialogFragment() {
private var onAddInstanceListener: OnAddInstanceListener? = null
private lateinit var oAuthData: OAuthData
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val binding = DataBindingUtil.inflate<AddInstanceBinding>(LayoutInflater.from(requireContext()), R.layout.add_instance,null,false)
val builder = AlertDialog.Builder(requireContext())
builder.setTitle(R.string.add_instance)
builder.setPositiveButton(R.string.connect, null)
builder.setNegativeButton(R.string.cancel) { dialog,_ -> dialog.dismiss() }
builder.setView(binding.root)
binding.errorUsername.visibility = View.GONE
binding.errorInstance.visibility = View.GONE
binding.errorPassword.visibility = View.GONE
binding.tryConnect.visibility = View.GONE
binding.tryConnectMsg.visibility = View.GONE
if (this::oAuthData.isInitialized)
{
binding.username.isEnabled = false
binding.instance.isEnabled = false
binding.instance.setText(oAuthData.baseUrl)
binding.username.setText(oAuthData.username)
}
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val username = binding.username.text.toString()
val password = binding.password.text.toString()
var instance = binding.instance.text.toString()
binding.errorUsername.visibility = View.GONE
binding.errorInstance.visibility = View.GONE
binding.errorPassword.visibility = View.GONE
binding.error.visibility = View.GONE
var inError = false
if(username.isEmpty())
{
binding.errorUsername.visibility = View.VISIBLE
inError = true
}
if(password.isEmpty())
{
binding.errorPassword.visibility = View.VISIBLE
inError = true
}
if(instance.isEmpty())
{
binding.errorInstance.visibility = View.VISIBLE
binding.errorInstance.setText(R.string.instance_error)
inError = true
} else {
if(!instance.startsWith("https://"))
instance = "https://$instance"
if (instance.endsWith("/"))
instance = instance.removeRange(instance.length-1,instance.length)
try {
URL(instance)
} catch (e: MalformedURLException) {
binding.errorInstance.visibility = View.VISIBLE
binding.errorInstance.setText(R.string.malformed_instance_error)
inError = true
}
}
if (DatabaseManager.existsCredential(requireContext(),instance,username)) {
inError = true
binding.error.visibility = View.VISIBLE
binding.error.text = requireContext().getString(R.string.account_exist)
}
if(!inError) {
binding.errorUsername.visibility = View.GONE
binding.errorInstance.visibility = View.GONE
binding.errorPassword.visibility = View.GONE
binding.error.visibility = View.GONE
binding.username.visibility = View.GONE
binding.password.visibility = View.GONE
binding.instance.visibility = View.GONE
binding.usernameTitle.visibility = View.GONE
binding.passwordTitle.visibility = View.GONE
binding.instanceTitle.visibility = View.GONE
binding.tryConnect.visibility = View.VISIBLE
binding.tryConnectMsg.visibility = View.VISIBLE
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false
val listener = object : InstanceManager.InstanceListener {
override fun onSuccess(args: Bundle?) {
val oauthData: OAuthData? = args?.getParcelable(InstanceManager.EXTRA_DATA)
if (oauthData != null) {
if (this@AddInstanceDialog::oAuthData.isInitialized) {
DatabaseManager.updateCredentials(requireContext(), oauthData)
} else {
DatabaseManager.addNewCredentials(requireContext(), oauthData)
}
onAddInstanceListener?.addSuccess(oauthData)
dialog.dismiss()
}
}
override fun onError(error: String?) {
Handler(Looper.getMainLooper()).post {
binding.error.visibility = View.VISIBLE
binding.tryConnect.visibility = View.GONE
binding.tryConnectMsg.visibility = View.GONE
binding.username.visibility = View.VISIBLE
binding.password.visibility = View.VISIBLE
binding.instance.visibility = View.VISIBLE
binding.usernameTitle.visibility = View.VISIBLE
binding.passwordTitle.visibility = View.VISIBLE
binding.instanceTitle.visibility = View.VISIBLE
binding.error.text = error
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = true
}
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
DatabaseManager.updateCredentials(requireContext(), oauthData)
}
};
if (this::oAuthData.isInitialized) {
InstanceManager.getUserToken(requireContext(), instance, username, password, oAuthData, listener)
} else {
InstanceManager.registerAccount(requireContext(), instance, username, password,listener)
}
}
}
}
return dialog
}
fun setOnAddInstanceListener(listener: OnAddInstanceListener) {
onAddInstanceListener = listener
}
fun setOauthData(oAuthData: OAuthData) {
this.oAuthData = oAuthData
}
interface OnAddInstanceListener {
fun addSuccess(oAuthData: OAuthData)
}
}

View File

@ -0,0 +1,73 @@
package fr.mobdev.peertubelive.manager
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class DatabaseHelper(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, 2) {
companion object {
const val DB_NAME: String = "peeriscope.db"
const val TABLE_CREDS: String = "Credentials"
const val CREDS_USERNAME: String = "Username"
const val CREDS_BASE_URL: String = "Instance"
const val CREDS_CLIENT_ID: String = "ClientID"
const val CREDS_CLIENT_SECRET: String = "ClientSecret"
const val CREDS_ACCESS_TOKEN: String = "AccessToken"
const val CREDS_TOKEN_TYPE: String = "TokenType"
const val CREDS_EXPIRES: String = "Expires"
const val CREDS_REFRESH_TOKEN: String = "RefreshToken"
const val CREDS_REFRESH_EXPIRES: String = "RefreshTokenExpires"
const val TABLE_STREAM_SETTINGS: String = "Settings"
const val SETS_TITLE: String = "Title"
const val SETS_CATEGORY: String = "Category"
const val SETS_PRIVACY : String = "Privacy"
const val SETS_LANGUAGE : String = "Language"
const val SETS_LICENCE : String = "Licence"
const val SETS_COMMENTS : String = "Comments"
const val SETS_DOWNLOAD : String = "Download"
const val SETS_REPLAY : String = "Replay"
const val SETS_NSFW : String = "Nsfw"
}
override fun onCreate(db: SQLiteDatabase?) {
db?.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_CREDS (id INTEGER PRIMARY KEY, $CREDS_USERNAME TEXT, $CREDS_BASE_URL TEXT, " +
"$CREDS_CLIENT_ID TEXT, $CREDS_CLIENT_SECRET TEXT, $CREDS_ACCESS_TOKEN TEXT, " +
"$CREDS_TOKEN_TYPE TEXT, $CREDS_EXPIRES INTEGER, $CREDS_REFRESH_TOKEN TEXT, $CREDS_REFRESH_EXPIRES INTEGER);")
db?.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_STREAM_SETTINGS (id INTEGER PRIMARY KEY, $SETS_TITLE TEXT, $SETS_CATEGORY INTEGER, $SETS_PRIVACY INTEGER, " +
"$SETS_LANGUAGE TEXT, $SETS_LICENCE INTEGER, $SETS_COMMENTS INTEGER, " +
"$SETS_DOWNLOAD INTEGER, $SETS_REPLAY INTEGER, $SETS_NSFW INTEGER);")
val values = ContentValues()
values.put("id",1)
values.put(SETS_TITLE,"Live")
values.put(SETS_COMMENTS,true)
values.put(SETS_DOWNLOAD,true)
values.put(SETS_NSFW,false)
values.put(SETS_REPLAY,false)
db?.insert(TABLE_STREAM_SETTINGS,null,values)
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
db?.execSQL("ALTER TABLE $TABLE_CREDS add column $CREDS_REFRESH_EXPIRES INTEGER;")
}
fun insert(table: String, values: ContentValues): Long {
return writableDatabase.insert(table,null,values)
}
fun update(table: String, values: ContentValues, whereClause: String?, whereArgs: Array<String?>): Int {
return writableDatabase.update(table,values,whereClause,whereArgs)
}
fun delete(table: String, whereClause: String?, whereArgs: Array<String?>?): Int {
return writableDatabase.delete(table,whereClause,whereArgs)
}
fun query(table: String, columns: Array<String>?, whereClause: String?, whereArgs: Array<String>?): Cursor {
return readableDatabase.query(table,columns,whereClause,whereArgs,null,null,null)
}
}

View File

@ -0,0 +1,132 @@
package fr.mobdev.peertubelive.manager
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import fr.mobdev.peertubelive.objects.OAuthData
import fr.mobdev.peertubelive.objects.StreamSettings
object DatabaseManager {
private var databaseHelper: DatabaseHelper? = null
fun addNewCredentials(context: Context, oAuthData: OAuthData) {
if (databaseHelper == null)
databaseHelper = DatabaseHelper(context)
val values = ContentValues()
values.put(DatabaseHelper.CREDS_BASE_URL,oAuthData.baseUrl)
values.put(DatabaseHelper.CREDS_USERNAME,oAuthData.username)
values.put(DatabaseHelper.CREDS_CLIENT_ID,oAuthData.clientId)
values.put(DatabaseHelper.CREDS_CLIENT_SECRET,oAuthData.clientSecret)
values.put(DatabaseHelper.CREDS_ACCESS_TOKEN,oAuthData.accessToken)
values.put(DatabaseHelper.CREDS_TOKEN_TYPE,oAuthData.tokenType)
values.put(DatabaseHelper.CREDS_EXPIRES,oAuthData.expires)
values.put(DatabaseHelper.CREDS_REFRESH_TOKEN,oAuthData.refreshToken)
values.put(DatabaseHelper.CREDS_REFRESH_EXPIRES,oAuthData.refreshTokenExpires)
databaseHelper?.insert(DatabaseHelper.TABLE_CREDS,values)
}
fun updateCredentials(context: Context, oAuthData: OAuthData) {
if (databaseHelper == null)
databaseHelper = DatabaseHelper(context)
val values = ContentValues()
val whereClause = "${DatabaseHelper.CREDS_USERNAME} = ? AND ${DatabaseHelper.CREDS_BASE_URL} = ? AND ${DatabaseHelper.CREDS_CLIENT_SECRET} = ?"
val whereArgs = arrayOf(oAuthData.username , oAuthData.baseUrl , oAuthData.clientSecret)
values.put(DatabaseHelper.CREDS_ACCESS_TOKEN,oAuthData.accessToken)
values.put(DatabaseHelper.CREDS_TOKEN_TYPE,oAuthData.tokenType)
values.put(DatabaseHelper.CREDS_EXPIRES,oAuthData.expires)
values.put(DatabaseHelper.CREDS_REFRESH_TOKEN,oAuthData.refreshToken)
values.put(DatabaseHelper.CREDS_REFRESH_EXPIRES,oAuthData.refreshTokenExpires)
databaseHelper?.update(DatabaseHelper.TABLE_CREDS,values,whereClause,whereArgs)
}
fun getCredentials(context: Context): List<OAuthData> {
if (databaseHelper == null)
databaseHelper = DatabaseHelper(context)
val oAuthDatas: ArrayList<OAuthData> = ArrayList()
val cursor: Cursor? = databaseHelper?.query(DatabaseHelper.TABLE_CREDS,null,null,null)
while (cursor?.moveToNext() == true) {
var col = 1
val username: String = cursor.getString(col++)
val baseUrl: String = cursor.getString(col++)
val clientId: String = cursor.getString(col++)
val clientSecret: String = cursor.getString(col++)
val accessToken: String = cursor.getString(col++)
val tokenType: String = cursor.getString(col++)
val expires: Long = cursor.getLong(col++)
val refreshToken: String = cursor.getString(col++)
val refreshTokenExpires: Long = cursor.getLong(col)
var oAuthData = OAuthData(baseUrl,username,clientId,clientSecret,accessToken,tokenType,expires,refreshToken,refreshTokenExpires)
oAuthDatas.add(oAuthData)
}
cursor?.close()
return oAuthDatas
}
fun existsCredential(context: Context, url: String, username: String): Boolean {
if (databaseHelper == null)
databaseHelper = DatabaseHelper(context)
val columns = arrayOf(DatabaseHelper.CREDS_BASE_URL, DatabaseHelper.CREDS_USERNAME)
val whereClause = "${DatabaseHelper.CREDS_USERNAME} = ? AND ${DatabaseHelper.CREDS_BASE_URL} = ?"
val whereArgs = arrayOf(username, url)
val cursor: Cursor? = databaseHelper?.query(DatabaseHelper.TABLE_CREDS,columns,whereClause,whereArgs)
val exist = cursor?.count != 0
cursor?.close()
return exist
}
fun deleteAccount(context: Context, oAuthData: OAuthData) {
if (databaseHelper == null)
databaseHelper = DatabaseHelper(context)
val whereClause = "${DatabaseHelper.CREDS_USERNAME} = ? AND ${DatabaseHelper.CREDS_BASE_URL} = ? AND ${DatabaseHelper.CREDS_CLIENT_ID} = ?"
val whereArgs = arrayOf(oAuthData.username, oAuthData.baseUrl, oAuthData.clientId)
databaseHelper?.delete(DatabaseHelper.TABLE_CREDS,whereClause,whereArgs)
}
fun getStreamSettings(context: Context): StreamSettings? {
if (databaseHelper == null)
databaseHelper = DatabaseHelper(context)
val cursor: Cursor? = databaseHelper?.query(DatabaseHelper.TABLE_STREAM_SETTINGS,null,null,null)
var streamSettings: StreamSettings? = null
if(cursor?.moveToNext() == true) {
var col = 1
val title: String = cursor.getString(col++)
val category: Int = cursor.getInt(col++)
val privacy: Int = cursor.getInt(col++)
val language: String? = cursor.getString(col++)
val licence: Int = cursor.getInt(col++)
val comments: Boolean = cursor.getInt(col++) == 1
val download: Boolean = cursor.getInt(col++) == 1
val saveReplay: Boolean = cursor.getInt(col++) == 1
val nsfw: Boolean = cursor.getInt(col) == 1
streamSettings = StreamSettings(title,0,privacy,category,language,licence,null,comments,download,nsfw,saveReplay)
}
cursor?.close()
return streamSettings
}
fun updateStreamSettings(context: Context, streamSettings: StreamSettings) {
if (databaseHelper == null)
databaseHelper = DatabaseHelper(context)
val values = ContentValues()
values.put(DatabaseHelper.SETS_TITLE,streamSettings.title)
values.put(DatabaseHelper.SETS_PRIVACY,streamSettings.privacy)
values.put(DatabaseHelper.SETS_CATEGORY,streamSettings.category)
values.put(DatabaseHelper.SETS_COMMENTS,streamSettings.comments)
values.put(DatabaseHelper.SETS_DOWNLOAD,streamSettings.download)
values.put(DatabaseHelper.SETS_NSFW,streamSettings.nsfw)
values.put(DatabaseHelper.SETS_REPLAY,streamSettings.saveReplay)
values.put(DatabaseHelper.SETS_LANGUAGE,streamSettings.language)
values.put(DatabaseHelper.SETS_LICENCE,streamSettings.licence)
val whereClause = "id = ?"
val whereArgs: Array<String?> = arrayOf("1")
databaseHelper?.update(DatabaseHelper.TABLE_STREAM_SETTINGS,values,whereClause,whereArgs)
}
}

View File

@ -0,0 +1,473 @@
package fr.mobdev.peertubelive.manager
import android.content.Context
import android.os.Bundle
import fr.mobdev.peertubelive.R
import fr.mobdev.peertubelive.objects.ChannelData
import fr.mobdev.peertubelive.objects.OAuthData
import fr.mobdev.peertubelive.objects.StreamData
import fr.mobdev.peertubelive.objects.StreamSettings
import org.json.JSONObject
import java.lang.Exception
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
object InstanceManager {
private val oauthManager : OAuthManager = OAuthManager()
private const val BASE_API_ENDPOINT: String = "/api/v1"
private const val REGISTER_CLIENT_ENDPOINT: String = "/oauth-clients/local"
private const val GET_USER_CLIENT_ENDPOINT: String = "/users/token"
private const val GET_USER_INFO_ENDPOINT: String = "/users/me"
private const val CREATE_LIVE_ENDPOINT: String = "/videos/live"
private const val GET_CATEGORY_ENDPOINT: String = "/videos/categories"
private const val GET_PRIVACY_ENDPOINT: String = "/videos/privacies"
private const val GET_LICENCE_ENDPOINT: String = "/videos/licences"
private const val GET_LANGUAGES_ENDPOINT: String = "/videos/languages"
private const val GET_VIDEOS: String = "/users/me/videos"
internal const val EXTRA_DATA: String = "EXTRA_DATA"
private const val CONTENT_TYPE: String = "CONTENT_TYPE"
private const val CONTENT_DATA: String = "CONTENT_DATA"
private const val VIDEO_CHANNEL: String = "videoChannels"
private const val CHANNEL_ID: String = "id"
private const val CHANNEL_NAME: String = "displayName"
private const val VIDEO: String = "video"
private const val UUID: String = "uuid"
private const val RTMP_URL: String = "rtmpUrl"
private const val STREAM_KEY: String = "streamKey"
fun registerAccount(context: Context, url: String, username: String, password: String, listener: InstanceListener) {
val registerUrl = url + BASE_API_ENDPOINT+ REGISTER_CLIENT_ENDPOINT
val internalListener: InstanceListener = object : InstanceListener {
override fun onSuccess(args: Bundle?) {
val oauthData: OAuthData? = args?.getParcelable(EXTRA_DATA)
oauthData?.baseUrl = url
if (oauthData != null)
getUserToken(context, url, username, password,oauthData, listener)
else
listener.onError(context.getString(R.string.unknwon_error))
}
override fun onError(error: String?) {
listener.onError(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
listener.onUpdateOAuthData(oauthData)
}
}
oauthManager.register(context,registerUrl,internalListener)
}
fun getUserToken(context: Context, url: String, username: String, password: String, oauthData: OAuthData, listener: InstanceListener) {
val userAccess = url + BASE_API_ENDPOINT + GET_USER_CLIENT_ENDPOINT
oauthManager.getUserToken(context, userAccess, username, password, oauthData, listener)
}
private fun refreshToken(context: Context, url: String, oauthData: OAuthData, listener: InstanceListener) {
val registerUrl = url + BASE_API_ENDPOINT+ GET_USER_CLIENT_ENDPOINT
oauthManager.refreshToken(context, registerUrl, oauthData, listener)
}
fun createLive(context: Context, url: String, oauthData: OAuthData, streamSettings: StreamSettings, listener: InstanceListener) {
if(oauthData.expires < Calendar.getInstance().timeInMillis) {
refreshToken(context,url,oauthData,object: InstanceListener {
override fun onSuccess(args: Bundle?) {
val oauth: OAuthData? = args?.getParcelable(InstanceManager.EXTRA_DATA)
if (oauth != null) {
DatabaseManager.updateCredentials(context,oauth)
listener.onUpdateOAuthData(oauth)
createLiveImpl(context, url,oauth,streamSettings,listener)
}
}
override fun onError(error: String?) {
listener.onError(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
listener.onUpdateOAuthData(oauthData)
}
})
} else {
createLiveImpl(context, url, oauthData,streamSettings,listener)
}
}
private fun getStreamKey(context: Context, url: String, oauthData: OAuthData, liveId: String, listener: InstanceListener) {
val liveInfo = "$url$BASE_API_ENDPOINT$CREATE_LIVE_ENDPOINT/$liveId"
val internalListener = object: InstanceListener {
override fun onSuccess(args: Bundle?) {
val response = args?.getString(EXTRA_DATA, null)
if (response == null) {
listener.onError(context.getString(R.string.unknwon_error))
return
}
val streamData = extractStreamData(response)
if (streamData == null) {
listener.onError(context.getString(R.string.json_error))
return
}
val results = Bundle()
results.putParcelable(EXTRA_DATA,streamData)
listener.onSuccess(results)
}
override fun onError(error: String?) {
listener.onError(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
listener.onUpdateOAuthData(oauthData)
}
}
oauthManager.get(context,liveInfo,oauthData,internalListener)
}
private fun createLiveImpl(context: Context, url: String, oauthData: OAuthData, streamSettings: StreamSettings, listener: InstanceListener) {
// val comments: Boolean, val download: Boolean, val nsfw: Boolean, val saveReplay: Boolean
val createLiveUrl = url + BASE_API_ENDPOINT + CREATE_LIVE_ENDPOINT
val data = Bundle()
val boundary = "45fcc22"
var formData: String = prepareFormData(boundary,"channelId",streamSettings.channel.toString(),false)
formData += prepareFormData(boundary,"name",streamSettings.title,false)
formData += prepareFormData(boundary,"privacy",streamSettings.privacy.toString(),false)
if(streamSettings.category != null)
formData += prepareFormData(boundary,"category",streamSettings.category.toString(),false)
if(streamSettings.language != null)
formData += prepareFormData(boundary,"language",streamSettings.language.toString(),false)
if(streamSettings.description != null)
formData += prepareFormData(boundary,"description",streamSettings.description.toString(),false)
if(streamSettings.licence != null)
formData += prepareFormData(boundary,"licence",streamSettings.licence.toString(),false)
formData += prepareFormData(boundary,"commentsEnabled",streamSettings.comments.toString(),false)
formData += prepareFormData(boundary,"nsfw",streamSettings.nsfw.toString(),false)
formData += prepareFormData(boundary,"downloadEnabled",streamSettings.download.toString(),false)
formData += prepareFormData(boundary,"saveReplay",streamSettings.saveReplay.toString(),true)
data.putString(CONTENT_TYPE,"multipart/form-data; boundary=$boundary")
data.putString(CONTENT_DATA,formData)
val internalListener = object: InstanceListener {
override fun onSuccess(args: Bundle?) {
val response = args?.getString(EXTRA_DATA, null)
if (response == null) {
listener.onError(context.getString(R.string.unknwon_error))
return
}
val liveId = extractLiveId(response)
if (liveId == null) {
listener.onError(context.getString(R.string.json_error))
return
}
getStreamKey(context,url,oauthData,liveId,listener)
}
override fun onError(error: String?) {
listener.onError(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
listener.onUpdateOAuthData(oauthData)
}
}
oauthManager.post(context,createLiveUrl,oauthData,data,internalListener)
}
fun getUserChannelList(context: Context, url: String, oauthData: OAuthData, listener: InstanceListener) {
if(oauthData.expires < Calendar.getInstance().timeInMillis) {
refreshToken(context,url,oauthData,object: InstanceListener {
override fun onSuccess(args: Bundle?) {
val oauth: OAuthData? = args?.getParcelable(EXTRA_DATA)
if (oauth != null) {
listener.onUpdateOAuthData(oauth)
DatabaseManager.updateCredentials(context,oauth)
getUserChannelListImpl(context, url,oauth,listener)
}
}
override fun onError(error: String?) {
listener.onError(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
listener.onUpdateOAuthData(oauthData)
}
})
} else {
getUserChannelListImpl(context, url, oauthData, listener)
}
}
private fun getUserChannelListImpl(context: Context, url: String, oauthData: OAuthData, listener: InstanceListener) {
val userInfoUrl: String = url + BASE_API_ENDPOINT + GET_USER_INFO_ENDPOINT
val internalListener : InstanceListener = object : InstanceListener {
override fun onSuccess(args: Bundle?) {
val response = args?.getString(EXTRA_DATA, null)
if (response == null) {
listener.onError(context.getString(R.string.unknwon_error))
return
}
val channelList = extractChannelData(response)
if (channelList == null) {
listener.onError(context.getString(R.string.json_error))
return
}
args.putParcelableArrayList(EXTRA_DATA, channelList)
listener.onSuccess(args)
}
override fun onError(error: String?) {
listener.onError(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
listener.onUpdateOAuthData(oauthData)
}
}
oauthManager.get(context,userInfoUrl,oauthData,internalListener)
}
fun getCategoryList(context: Context, url: String, listener: InstanceListener) {
val userInfoUrl: String = url + BASE_API_ENDPOINT + GET_CATEGORY_ENDPOINT
val internalListener : InstanceListener = object : InstanceListener {
override fun onSuccess(args: Bundle?) {
val response = args?.getString(EXTRA_DATA, null)
if (response == null) {
listener.onError(context.getString(R.string.unknwon_error))
return
}
val categoryList = extractMapData<Int>(response,0)
if (categoryList == null) {
listener.onError(context.getString(R.string.json_error))
return
}
categoryList[""]=0
args.putSerializable(EXTRA_DATA, categoryList)
listener.onSuccess(args)
}
override fun onError(error: String?) {
listener.onError(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
listener.onUpdateOAuthData(oauthData)
}
}
oauthManager.get(context,userInfoUrl,null,internalListener)
}
fun getPrivacyList(context: Context, url: String, listener: InstanceListener) {
val userInfoUrl: String = url + BASE_API_ENDPOINT + GET_PRIVACY_ENDPOINT
val internalListener : InstanceListener = object : InstanceListener {
override fun onSuccess(args: Bundle?) {
val response = args?.getString(EXTRA_DATA, null)
if (response == null) {
listener.onError(context.getString(R.string.unknwon_error))
return
}
val privacyList = extractMapData<Int>(response,0)
if (privacyList == null) {
listener.onError(context.getString(R.string.json_error))
return
}
args.putSerializable(EXTRA_DATA, privacyList)
listener.onSuccess(args)
}
override fun onError(error: String?) {
listener.onError(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
listener.onUpdateOAuthData(oauthData)
}
}
oauthManager.get(context,userInfoUrl,null,internalListener)
}
fun getLicencesList(context: Context, url: String, listener: InstanceListener) {
val userInfoUrl: String = url + BASE_API_ENDPOINT + GET_LICENCE_ENDPOINT
val internalListener : InstanceListener = object : InstanceListener {
override fun onSuccess(args: Bundle?) {
val response = args?.getString(EXTRA_DATA, null)
if (response == null) {
listener.onError(context.getString(R.string.unknwon_error))
return
}
val licencesList = extractMapData<Int>(response,0)
if (licencesList == null) {
listener.onError(context.getString(R.string.json_error))
return
}
licencesList[""]=0
args.putSerializable(EXTRA_DATA, licencesList)
listener.onSuccess(args)
}
override fun onError(error: String?) {
listener.onError(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
listener.onUpdateOAuthData(oauthData)
}
}
oauthManager.get(context,userInfoUrl,null,internalListener)
}
fun getLanguageList(context: Context, url: String, listener: InstanceListener) {
val userInfoUrl: String = url + BASE_API_ENDPOINT + GET_LANGUAGES_ENDPOINT
val internalListener : InstanceListener = object : InstanceListener {
override fun onSuccess(args: Bundle?) {
val response = args?.getString(EXTRA_DATA, null)
if (response == null) {
listener.onError(context.getString(R.string.unknwon_error))
return
}
val languageList = extractMapData<String>(response,"")
if (languageList == null) {
listener.onError(context.getString(R.string.json_error))
return
}
languageList[""]=""
args.putSerializable(EXTRA_DATA, languageList)
listener.onSuccess(args)
}
override fun onError(error: String?) {
listener.onError(error)
}
override fun onUpdateOAuthData(oauthData: OAuthData) {
listener.onUpdateOAuthData(oauthData)
}
}
oauthManager.get(context,userInfoUrl,null,internalListener)
}
private fun extractChannelData(response: String): ArrayList<ChannelData>? {
try {
val json = JSONObject(response)
if (json.has(VIDEO_CHANNEL)) {
val channelList: ArrayList<ChannelData> = ArrayList()
val channels = json.getJSONArray(VIDEO_CHANNEL)
for (i: Int in 0 until channels.length()) {
val channel = channels.getJSONObject(i)
if (channel.has(CHANNEL_NAME) && channel.has(CHANNEL_ID)) {
val name = channel.getString(CHANNEL_NAME)
val id = channel.getLong(CHANNEL_ID)
val channelData = ChannelData(id, name)
channelList.add(channelData)
} else {
return null
}
}
return channelList
} else {
return null
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
private fun extractLiveId(response: String): String? {
try {
val json = JSONObject(response)
return if(json.has(VIDEO)) {
val video = json.getJSONObject(VIDEO)
if (video.has(UUID)) {
video.getString(UUID)
} else {
null
}
} else {
null
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
private fun extractStreamData(response: String): StreamData? {
try {
val json = JSONObject(response)
return if (json.has(RTMP_URL) && json.has(STREAM_KEY)) {
val rtmp = json.getString(RTMP_URL)
val key = json.getString(STREAM_KEY)
StreamData(rtmp,key)
} else {
null
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
private fun <T> extractMapData(response: String, type: T): HashMap<String,T>? {
return try {
val json = JSONObject(response)
val map = HashMap<String,T>()
for(key in json.keys()) {
if (type is Int)
map[json.getString(key)] = key.toInt() as T
else if (type is String)
map[json.getString(key)] = key as T
}
map
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private fun prepareFormData(boundary: String, propertyName: String, property: String, lastData: Boolean): String {
val crlf = "\r\n"
var formData = "--$boundary$crlf"
formData+="Content-Disposition: form-data; name=\"$propertyName\"$crlf$crlf"
formData+="$property$crlf"
if(lastData) {
formData += "$crlf--$boundary--$crlf"
}
return formData
}
interface InstanceListener{
fun onSuccess(args: Bundle?)
fun onError(error: String?)
fun onUpdateOAuthData(oauthData: OAuthData)
}
}

View File

@ -0,0 +1,532 @@
package fr.mobdev.peertubelive.manager
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Bundle
import fr.mobdev.peertubelive.BuildConfig
import fr.mobdev.peertubelive.R
import fr.mobdev.peertubelive.objects.OAuthData
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
import java.io.OutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.net.UnknownHostException
import java.util.*
import java.util.concurrent.Semaphore
import kotlin.collections.ArrayList
class OAuthManager {
constructor() {
start()
}
fun register(context: Context, url: String, listener: InstanceManager.InstanceListener) {
val args = Bundle()
args.putString(URL, url)
val message = Message()
message.type = Message.Message_Type.REGISTER
message.context = context
message.args = args
message.listener = listener
addMessage(message)
}
fun getUserToken(context: Context, url: String, username: String, password: String, oauthData: OAuthData, listener: InstanceManager.InstanceListener) {
val args = Bundle()
args.putString(URL, url)
args.putParcelable(OAUTH_DATA, oauthData)
args.putString(USERNAME, username)
args.putString(PASSWORD, password)
val message = Message()
message.type = Message.Message_Type.GET_USER_TOKEN
message.context = context
message.args = args
message.listener = listener
addMessage(message)
}
fun refreshToken(context: Context, url: String, oauthData: OAuthData, listener: InstanceManager.InstanceListener) {
val args = Bundle()
args.putString(URL, url)
args.putParcelable(OAUTH_DATA, oauthData)
val message = Message()
message.type = Message.Message_Type.REFRESH_TOKEN
message.context = context
message.args = args
message.listener = listener
addMessage(message)
}
fun post(context: Context, url: String, oauthData: OAuthData, data: Bundle, listener: InstanceManager.InstanceListener) {
val args = Bundle()
args.putString(URL, url)
args.putParcelable(OAUTH_DATA, oauthData)
args.putBundle(DATA, data)
val message = Message()
message.type = Message.Message_Type.POST
message.context = context
message.args = args
message.listener = listener
addMessage(message)
}
fun get(context: Context, url: String, oauthData: OAuthData?, listener: InstanceManager.InstanceListener) {
val args = Bundle()
args.putString(URL, url)
args.putParcelable(OAUTH_DATA, oauthData)
val message = Message()
message.type = Message.Message_Type.GET
message.context = context
message.args = args
message.listener = listener
addMessage(message)
}
private companion object OAuthThread : Thread() {
private val messageQueue: ArrayList<Message> = ArrayList()
private val sem: Semaphore = Semaphore(0, true)
private const val URL: String = "URL"
private const val USERNAME: String = "USERNAME"
private const val PASSWORD: String = "PASSWORD"
private const val OAUTH_DATA: String = "OAUTH_DATA"
private const val DATA: String = "DATA"
private const val EXTRA_DATA: String = "EXTRA_DATA"
private const val CONTENT_TYPE: String = "CONTENT_TYPE"
private const val CONTENT_DATA: String = "CONTENT_DATA"
fun addMessage(message: Message)
{
messageQueue.add(message)
sem.release()
}
override fun run() {
var isRunning = true
while (isRunning) {
try {
sem.acquire()
val mes: Message = messageQueue.removeAt(0)
when (mes.type) {
Message.Message_Type.REGISTER -> register(mes)
Message.Message_Type.GET_USER_TOKEN -> getUserToken(mes)
Message.Message_Type.REFRESH_TOKEN -> refreshToken(mes)
Message.Message_Type.POST -> post(mes)
Message.Message_Type.GET -> get(mes)
else -> {}
}
} catch (e: InterruptedException) {
isRunning = false
}
}
}
fun register(message: Message) {
if (!isConnectedToInternet(message.context)) {
message.listener?.onError(message.context.getString(R.string.network_error))
return
}
val url: String = message.args.getString(URL,"")
val registerUrl = URL(url)
val connection: HttpURLConnection = registerUrl.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty("User-Agent", "Peeriscope")
connection.setRequestProperty("Content-Type", "application/json")
connection.setRequestProperty("Accept", "application/json")
connection.doInput = true
var inputStream: InputStream
var inError = false
try {
inputStream = connection.inputStream
} catch (e: UnknownHostException) {
message.listener?.onError(message.context.getString(R.string.unknown_host))
return
} catch (e : Exception) {
e.printStackTrace()
inputStream = connection.errorStream
inError = true
}
val response = readInputStream(inputStream)
if(!inError) {
if(response.isNotEmpty()) {
val rootObj = JSONObject(response)
val clientId = rootObj.getString("client_id")
val clientSecret = rootObj.getString("client_secret")
val oauthData = OAuthData(null, null, clientId, clientSecret, null, null, 0, null,0)
val result = Bundle()
result.putParcelable(EXTRA_DATA, oauthData)
message.listener?.onSuccess(result)
} else {
message.listener?.onError(message.context.getString(R.string.unknwon_error))
}
} else {
if(response.isNotEmpty()) {
val rootObj = JSONObject(response)
val error = rootObj.getString("error")
message.listener?.onError(error)
} else {
message.listener?.onError(message.context.getString(R.string.unknwon_error))
}
}
}
fun getUserToken(message: Message) {
if (!isConnectedToInternet(message.context)) {
message.listener?.onError(message.context.getString(R.string.network_error))
return
}
val url: String = message.args.getString(URL, "")
val getUserTokenUrl = URL(url)
val connection: HttpURLConnection = getUserTokenUrl.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("User-Agent", "Peeriscope")
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
connection.setRequestProperty("Accept", "application/json")
connection.doInput = true
connection.doOutput = true
val oauth: OAuthData? = message.args.getParcelable(OAUTH_DATA)
if (BuildConfig.DEBUG && oauth == null) {
error("Missing OAUTH DATA")
}
val username: String = message.args.getString(USERNAME,"")
val password: String = message.args.getString(PASSWORD,"")
var output = ""
output += "client_id=" + (oauth?.clientId ?: "")
output += "&client_secret=" + (oauth?.clientSecret ?: "")
output += "&grant_type=password"
output += "&response_type=code"
output += "&username=$username"
output += "&password=$password"
val outputStream = connection.outputStream
outputStream.write(output.toByteArray())
var inputStream: InputStream
var inError = false
try {
inputStream = connection.inputStream
} catch (e: UnknownHostException) {
message.listener?.onError(message.context.getString(R.string.unknown_host))
return
} catch (e : Exception) {
e.printStackTrace()
inputStream = connection.errorStream
inError = true
}
val response = readInputStream(inputStream)
if(!inError) {
if(response.isNotEmpty()) {
val rootObj = JSONObject(response)
val accessToken = rootObj.getString("access_token")
val tokenType = rootObj.getString("token_type")
var expires = Calendar.getInstance().timeInMillis
expires += rootObj.getLong("expires_in")*1000
val refreshToken = rootObj.getString("refresh_token")
var refreshTokenExpires = Calendar.getInstance().timeInMillis
refreshTokenExpires += rootObj.getLong("refresh_token_expires_in")*1000
val oauthData = OAuthData(oauth?.baseUrl,username,oauth?.clientId, oauth?.clientSecret, accessToken, tokenType, expires, refreshToken, refreshTokenExpires)
val result = Bundle()
result.putParcelable(EXTRA_DATA, oauthData)
message.listener?.onSuccess(result)
} else {
message.listener?.onError(message.context.getString(R.string.unknwon_error))
}
} else {
if(response.isNotEmpty()) {
val rootObj = JSONObject(response)
val error = rootObj.getString("error")
message.listener?.onError(error)
} else {
message.listener?.onError(message.context.getString(R.string.unknwon_error))
}
}
}
fun refreshToken(message: Message) {
if (!isConnectedToInternet(message.context)) {
message.listener?.onError(message.context.getString(R.string.network_error))
return
}
val url: String = message.args.getString(URL, "")
val getUserTokenUrl = URL(url)
val connection: HttpURLConnection = getUserTokenUrl.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("User-Agent", "Peeriscope")
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
connection.setRequestProperty("Accept", "application/json")
connection.doInput = true
connection.doOutput = true
val oauth: OAuthData? = message.args.getParcelable(OAUTH_DATA)
if (BuildConfig.DEBUG && oauth == null) {
error("Missing OAUTH DATA")
}
var output = ""
output += "client_id=" + (oauth?.clientId ?: "")
output += "&client_secret=" + (oauth?.clientSecret ?: "")
output += "&grant_type=refresh_token"
output += "&response_type=code"
output += "&refresh_token=" +(oauth?.refreshToken?: "")
val outputStream = connection.outputStream
outputStream.write(output.toByteArray())
var inputStream: InputStream
var inError = false
try {
inputStream = connection.inputStream
} catch (e: UnknownHostException) {
message.listener?.onError(message.context.getString(R.string.unknown_host))
return
} catch (e : Exception) {
e.printStackTrace()
inputStream = connection.errorStream
inError = true
}
val response = readInputStream(inputStream)
if(!inError) {
if(response.isNotEmpty()) {
val rootObj = JSONObject(response)
val accessToken = rootObj.getString("access_token")
val tokenType = rootObj.getString("token_type")
var expires = Calendar.getInstance().timeInMillis
expires += rootObj.getLong("expires_in")*1000
val refreshToken = rootObj.getString("refresh_token")
var refreshTokenExpires = Calendar.getInstance().timeInMillis
refreshTokenExpires += rootObj.getLong("refresh_token_expires_in")*1000
val oauthData = OAuthData(oauth?.baseUrl,oauth?.username,oauth?.clientId, oauth?.clientSecret, accessToken, tokenType, expires, refreshToken, refreshTokenExpires)
val result = Bundle()
result.putParcelable(EXTRA_DATA, oauthData)
message.listener?.onSuccess(result)
} else {
message.listener?.onError(message.context.getString(R.string.unknwon_error))
}
} else {
if(response.isNotEmpty()) {
val rootObj = JSONObject(response)
val error = rootObj.getString("error")
message.listener?.onError(error)
} else {
message.listener?.onError(message.context.getString(R.string.unknwon_error))
}
}
}
fun post(message: Message) {
if (!isConnectedToInternet(message.context)) {
message.listener?.onError(message.context.getString(R.string.network_error))
return
}
val url: String = message.args.getString(URL, "")
val postUrl = URL(url)
val data = message.args.getBundle(DATA)!!
val connection: HttpURLConnection = postUrl.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("User-Agent", "Peeriscope")
connection.setRequestProperty("Content-Type", data.getString(CONTENT_TYPE,"application/json"))
connection.setRequestProperty("Accept", "application/json")
connection.doInput = true
connection.doOutput = true
val oauth: OAuthData? = message.args.getParcelable(OAUTH_DATA)
if (BuildConfig.DEBUG && oauth == null) {
error("Missing OAUTH DATA")
}
val extraData: String = data.getString(CONTENT_DATA,"")
connection.setRequestProperty("Authorization","Bearer ${oauth?.accessToken}")
var inputStream: InputStream? = null
val outputStream: OutputStream
var inError = false
try {
outputStream = connection.outputStream
outputStream.write(extraData.toByteArray())
outputStream.flush()
outputStream.close()
} catch (e: UnknownHostException) {
message.listener?.onError(message.context.getString(R.string.unknown_host))
return
} catch (e : Exception) {
e.printStackTrace()
inError = true
inputStream = connection.errorStream
}
if(!inError) {
try {
inputStream = connection.inputStream
} catch (e: UnknownHostException) {
message.listener?.onError(message.context.getString(R.string.unknown_host))
return
} catch (e: Exception) {
e.printStackTrace()
inputStream = connection.errorStream
inError = true
}
}
if (inputStream != null) {
val response = readInputStream(inputStream)
if (!inError) {
if (response.isNotEmpty()) {
val result = Bundle()
result.putString(EXTRA_DATA, response)
message.listener?.onSuccess(result)
} else {
message.listener?.onError(message.context.getString(R.string.unknwon_error))
}
} else {
if (response.isNotEmpty()) {
try {
val rootObj = JSONObject(response)
val error = rootObj.getString("error")
message.listener?.onError(error)
} catch (e: Exception) {
message.listener?.onError(message.context.getString(R.string.json_error))
}
} else {
message.listener?.onError(message.context.getString(R.string.unknwon_error))
}
}
}
}
fun get(message: Message) {
if (!isConnectedToInternet(message.context)) {
message.listener?.onError(message.context.getString(R.string.network_error))
return
}
val url: String = message.args.getString(URL, "")
val getUserTokenUrl = URL(url)
val connection: HttpURLConnection = getUserTokenUrl.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty("User-Agent", "Peeriscope")
connection.setRequestProperty("Content-Type", "application/json")
connection.setRequestProperty("Accept", "application/json")
connection.doInput = true
val oauth: OAuthData? = message.args.getParcelable(OAUTH_DATA)
if (oauth != null) {
connection.setRequestProperty("Authorization", "Bearer ${oauth.accessToken}")
}
var inputStream: InputStream
var inError = false
try {
inputStream = connection.inputStream
} catch (e: UnknownHostException) {
message.listener?.onError(message.context.getString(R.string.unknown_host))
return
} catch (e : Exception) {
e.printStackTrace()
inputStream = connection.errorStream
inError = true
}
val response = readInputStream(inputStream)
if(!inError) {
if(response.isNotEmpty()) {
val result = Bundle()
result.putString(EXTRA_DATA, response)
message.listener?.onSuccess(result)
} else {
message.listener?.onError(message.context.getString(R.string.unknwon_error))
}
} else {
if(response.isNotEmpty()) {
try {
val rootObj = JSONObject(response)
val error = rootObj.getString("error")
message.listener?.onError(error)
} catch (e: Exception) {
message.listener?.onError(message.context.getString(R.string.json_error))
}
} else {
message.listener?.onError(message.context.getString(R.string.unknwon_error))
}
}
}
fun readInputStream(inputStream: InputStream) : String {
val inReader = InputStreamReader(inputStream)
val bufReader = BufferedReader(inReader)
var line: String?
var response = ""
do {
line = bufReader.readLine()
if(line != null)
response += line
}while (line != null)
return response
}
private fun isConnectedToInternet(context: Context): Boolean {
//verify the connectivity
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
if(capabilities != null)
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) || capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} else {
val networkInfo = connectivityManager.activeNetworkInfo
if (networkInfo != null)
return networkInfo.isConnected
}
return false
}
}
private class Message {
var type: Message_Type = Message_Type.UNKNOWN
var args = Bundle()
var listener: InstanceManager.InstanceListener? = null
lateinit var context: Context
enum class Message_Type {
REGISTER, GET_USER_TOKEN, REFRESH_TOKEN, POST, GET, UNKNOWN
}
}
}

View File

@ -0,0 +1,32 @@
package fr.mobdev.peertubelive.objects
import android.os.Parcel
import android.os.Parcelable
class ChannelData(val id: Long, val name: String?): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readLong(),
parcel.readString()
) {
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeLong(id)
dest.writeString(name)
}
companion object CREATOR : Parcelable.Creator<ChannelData> {
override fun createFromParcel(parcel: Parcel): ChannelData {
return ChannelData(parcel)
}
override fun newArray(size: Int): Array<ChannelData?> {
return arrayOfNulls(size)
}
}
}

View File

@ -0,0 +1,58 @@
package fr.mobdev.peertubelive.objects
import android.os.Parcel
import android.os.Parcelable
class OAuthData(var baseUrl: String?, var username: String?, var clientId: String?, var clientSecret: String?, var accessToken: String?, var tokenType: String?, var expires: Long, var refreshToken: String?, var refreshTokenExpires: Long) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readLong(),
parcel.readString(),
parcel.readLong()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(baseUrl)
parcel.writeString(username)
parcel.writeString(clientId)
parcel.writeString(clientSecret)
parcel.writeString(accessToken)
parcel.writeString(tokenType)
parcel.writeLong(expires)
parcel.writeString(refreshToken)
parcel.writeLong(refreshTokenExpires)
}
override fun describeContents(): Int {
return 0
}
fun updateData(oAuthData: OAuthData){
baseUrl = oAuthData.baseUrl
username = oAuthData.username
clientId = oAuthData.clientId
clientSecret = oAuthData.clientSecret
accessToken = oAuthData.accessToken
tokenType = oAuthData.tokenType
expires = oAuthData.expires
refreshToken = oAuthData.refreshToken
refreshTokenExpires = oAuthData.refreshTokenExpires
}
companion object CREATOR : Parcelable.Creator<OAuthData> {
override fun createFromParcel(parcel: Parcel): OAuthData {
return OAuthData(parcel)
}
override fun newArray(size: Int): Array<OAuthData?> {
return arrayOfNulls(size)
}
}
}

View File

@ -0,0 +1,32 @@
package fr.mobdev.peertubelive.objects
import android.os.Parcel
import android.os.Parcelable
class StreamData(val url: String?, val key: String?): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString()
) {
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(url)
dest.writeString(key)
}
companion object CREATOR : Parcelable.Creator<StreamData> {
override fun createFromParcel(parcel: Parcel): StreamData {
return StreamData(parcel)
}
override fun newArray(size: Int): Array<StreamData?> {
return arrayOfNulls(size)
}
}
}

View File

@ -0,0 +1,55 @@
package fr.mobdev.peertubelive.objects
import android.os.Parcel
import android.os.Parcelable
class StreamSettings(
val title: String, val channel: Long, val privacy: Int, val category: Int?, val language: String?, val licence: Int?, val description: String?,
val comments: Boolean, val download: Boolean, val nsfw: Boolean, val saveReplay: Boolean) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString()!!,
parcel.readLong(),
parcel.readInt(),
parcel.readInt(),
parcel.readString(),
parcel.readInt(),
parcel.readString(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(title)
parcel.writeLong(channel)
parcel.writeInt(privacy)
if (category != null) {
parcel.writeInt(category)
}
parcel.writeString(language)
if (licence != null) {
parcel.writeInt(licence)
}
parcel.writeString(description)
parcel.writeByte(if (comments) 1 else 0)
parcel.writeByte(if (download) 1 else 0)
parcel.writeByte(if (nsfw) 1 else 0)
parcel.writeByte(if (saveReplay) 1 else 0)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<StreamSettings> {
override fun createFromParcel(parcel: Parcel): StreamSettings {
return StreamSettings(parcel)
}
override fun newArray(size: Int): Array<StreamSettings?> {
return arrayOfNulls(size)
}
}
}

View File

@ -0,0 +1,271 @@
package fr.mobdev.peertubelive.utils
import fr.mobdev.peertubelive.R
class TranslationUtils {
companion object {
private val categoryMap: Map<String, Int> = mapOf(
"Music" to R.string.music,
"Films" to R.string.films,
"Vehicles" to R.string.vehicles,
"Art" to R.string.art,
"Sports" to R.string.sports,
"Travels" to R.string.travels,
"Gaming" to R.string.gaming,
"People" to R.string.people,
"Comedy" to R.string.comedy,
"Entertainment" to R.string.entertainment,
"News & Politics" to R.string.news_politics,
"How To" to R.string.how_to,
"Education" to R.string.education,
"Activism" to R.string.activism,
"Science & Technology" to R.string.science_tech,
"Animals" to R.string.animals,
"Kids" to R.string.kids,
"Food" to R.string.food
)
fun getCategoryTranslationFor(category: String):Int {
return if (categoryMap.containsKey(category))
categoryMap[category]!!
else
-1
}
private val licenceMap: Map<String, Int> = mapOf(
"Attribution" to R.string.by,
"Attribution - Share Alike" to R.string.bysa,
"Attribution - No Derivatives" to R.string.bynd,
"Attribution - Non Commercial" to R.string.bync,
"Attribution - Non Commercial - Share Alike" to R.string.byncsa,
"Attribution - Non Commercial - No Derivatives" to R.string.byncnd,
"Public Domain Dedication" to R.string.public_domain
)
fun getLicenceTranslationFor(licence: String):Int {
return if (licenceMap.containsKey(licence))
licenceMap[licence]!!
else
-1
}
private val privacyMap: Map<String, Int> = mapOf(
"Public" to R.string.privacy_public,
"Private" to R.string.privacy_private,
"Internal" to R.string.internal,
"Unlisted" to R.string.unlisted
)
fun getPrivacyTranslationFor(privacy: String):Int {
return if (privacyMap.containsKey(privacy))
privacyMap[privacy]!!
else
-1
}
private val languageMap: Map<String, Int> = mapOf(
"Afar" to R.string.afar,
"Abkhazian" to R.string.abkhazian,
"Afrikaans" to R.string.afrikaans,
"Akan" to R.string.akan,
"Amharic" to R.string.amharic,
"Arabic" to R.string.arabic,
"Aragonese" to R.string.aragonese,
"American Sign Language" to R.string.american_sign_language,
"Assamese" to R.string.assamese,
"Avaric" to R.string.avaric,
"Kotava" to R.string.kotava,
"Aymara" to R.string.aymara,
"Azerbaijani" to R.string.azerbaijani,
"Bashkir" to R.string.bashkir,
"Bambara" to R.string.bambara,
"Belarusian" to R.string.belarusian,
"Bengali" to R.string.bengali,
"British Sign Language" to R.string.british_sign_language,
"Bislama" to R.string.bislama,
"Tibetan" to R.string.tibetan,
"Bosnian" to R.string.bosnian,
"Breton" to R.string.breton,
"Bulgarian" to R.string.bulgarian,
"Brazilian Sign Language" to R.string.brazilian_sign_language,
"Catalan" to R.string.catalan,
"Czech" to R.string.czech,
"Chamorro" to R.string.chamorro,
"Chechen" to R.string.chechen,
"Chuvash" to R.string.chuvash,
"Cornish" to R.string.cornish,
"Corsican" to R.string.corsican,
"Cree" to R.string.cree,
"Czech Sign Language" to R.string.czech_sign_language,
"Chinese Sign Language" to R.string.chinese_sign_language,
"Welsh" to R.string.welsh,
"Danish" to R.string.danish,
"German" to R.string.german,
"Dhivehi" to R.string.dhivehi,
"Danish Sign Language" to R.string.danish_sign_language,
"Dzongkha" to R.string.dzongkha,
"Greek" to R.string.greek,
"English" to R.string.english,
"Esperanto" to R.string.esperanto,
"Estonian" to R.string.estonian,
"Basque" to R.string.basque,
"Ewe" to R.string.ewe,
"Faroese" to R.string.faroese,
"Persian" to R.string.persian,
"Fijian" to R.string.fijian,
"Finnish" to R.string.finnish,
"French" to R.string.french,
"Western Frisian" to R.string.western_frisian,
"French Sign Language" to R.string.french_sign_language,
"Fulah" to R.string.fulah,
"Scottish Gaelic" to R.string.scottish_gaelic,
"Irish" to R.string.irish,
"Galician" to R.string.galician,
"Manx" to R.string.manx,
"Guarani" to R.string.guarani,
"German Sign Language" to R.string.german_sign_language,
"Gujarati" to R.string.gujarati,
"Haitian" to R.string.haitian,
"Hausa" to R.string.hausa,
"Serbo-Croatian" to R.string.serbo_croatian,
"Hebrew" to R.string.hebrew,
"Herero" to R.string.herero,
"Hindi" to R.string.hindi,
"Hiri Motu" to R.string.hiri_motu,
"Croatian" to R.string.croatian,
"Hungarian" to R.string.hungarian,
"Armenian" to R.string.armenian,
"Igbo" to R.string.igbo,
"Sichuan Yi" to R.string.sichuan_yi,
"Inuktitut" to R.string.inuktitut,
"Indonesian" to R.string.indonesian,
"Inupiaq" to R.string.inupiaq,
"Icelandic" to R.string.icelandic,
"Italian" to R.string.italian,
"Javanese" to R.string.javanese,
"Lojban" to R.string.lojban,
"Japanese" to R.string.japanese,
"Japanese Sign Language" to R.string.japanese_sign_language,
"Kabyle" to R.string.kabyle,
"Kalaallisut" to R.string.kalaallisut,
"Kannada" to R.string.kannada,
"Kashmiri" to R.string.kashmiri,
"Georgian" to R.string.georgian,
"Kanuri" to R.string.kanuri,
"Kazakh" to R.string.kazakh,
"Khmer" to R.string.khmer,
"Kikuyu" to R.string.kikuyu,
"Kinyarwanda" to R.string.kinyarwanda,
"Kirghiz" to R.string.kirghiz,
"Komi" to R.string.komi,
"Kongo" to R.string.kongo,
"Korean" to R.string.korean,
"Kuanyama" to R.string.kuanyama,
"Kurdish" to R.string.kurdish,
"Lao" to R.string.lao,
"Latvian" to R.string.latvian,
"Limburgan" to R.string.limburgan,
"Lingala" to R.string.lingala,
"Lithuanian" to R.string.lithuanian,
"Luxembourgish" to R.string.luxembourgish,
"Luba-Katanga" to R.string.luba_katanga,
"Ganda" to R.string.ganda,
"Marshallese" to R.string.marshallese,
"Malayalam" to R.string.malayalam,
"Marathi" to R.string.marathi,
"Macedonian" to R.string.macedonian,
"Malagasy" to R.string.malagasy,
"Maltese" to R.string.maltese,
"Mongolian" to R.string.mongolian,
"Maori" to R.string.maori,
"Malay (macrolanguage)" to R.string.malay_macrolanguage,
"Burmese" to R.string.burmese,
"Nauru" to R.string.nauru,
"Navajo" to R.string.navajo,
"South Ndebele" to R.string.south_ndebele,
"North Ndebele" to R.string.north_ndebele,
"Ndonga" to R.string.ndonga,
"Nepali (macrolanguage)" to R.string.nepali_macrolanguage,
"Dutch" to R.string.dutch,
"Norwegian Nynorsk" to R.string.norwegian_nynorsk,
"Norwegian Bokmål" to R.string.norwegian_bokmål,
"Norwegian" to R.string.norwegian,
"Nyanja" to R.string.nyanja,
"Occitan" to R.string.occitan,
"Ojibwa" to R.string.ojibwa,
"Oriya (macrolanguage)" to R.string.oriya_macrolanguage,
"Oromo" to R.string.oromo,
"Ossetian" to R.string.ossetian,
"Panjabi" to R.string.panjabi,
"Pakistan Sign Language" to R.string.pakistan_sign_language,
"Polish" to R.string.polish,
"Portuguese" to R.string.portuguese,
"Pushto" to R.string.pushto,
"Quechua" to R.string.quechua,
"Romansh" to R.string.romansh,
"Romanian" to R.string.romanian,
"Russian Sign Language" to R.string.russian_sign_language,
"Rundi" to R.string.rundi,
"Russian" to R.string.russian,
"Sango" to R.string.sango,
"Saudi Arabian Sign Language" to R.string.saudi_arabian_sign_language,
"South African Sign Language" to R.string.south_african_sign_language,
"Sinhala" to R.string.sinhala,
"Slovak" to R.string.slovak,
"Slovenian" to R.string.slovenian,
"Northern Sami" to R.string.northern_sami,
"Samoan" to R.string.samoan,
"Shona" to R.string.shona,
"Sindhi" to R.string.sindhi,
"Somali" to R.string.somali,
"Southern Sotho" to R.string.southern_sotho,
"Spanish" to R.string.spanish,
"Albanian" to R.string.albanian,
"Sardinian" to R.string.sardinian,
"Serbian" to R.string.serbian,
"Swati" to R.string.swati,
"Sundanese" to R.string.sundanese,
"Swahili (macrolanguage)" to R.string.swahili_macrolanguage,
"Swedish" to R.string.swedish,
"Swedish Sign Language" to R.string.swedish_sign_language,
"Tahitian" to R.string.tahitian,
"Tamil" to R.string.tamil,
"Tatar" to R.string.tatar,
"Telugu" to R.string.telugu,
"Tajik" to R.string.tajik,
"Tagalog" to R.string.tagalog,
"Thai" to R.string.thai,
"Tigrinya" to R.string.tigrinya,
"Klingon" to R.string.klingon,
"Tonga (Tonga Islands)" to R.string.tonga_tonga_islands,
"Tswana" to R.string.tswana,
"Tsonga" to R.string.tsonga,
"Turkmen" to R.string.turkmen,
"Turkish" to R.string.turkish,
"Twi" to R.string.twi,
"Uighur" to R.string.uighur,
"Ukrainian" to R.string.ukrainian,
"Urdu" to R.string.urdu,
"Uzbek" to R.string.uzbek,
"Venda" to R.string.venda,
"Vietnamese" to R.string.vietnamese,
"Walloon" to R.string.walloon,
"Wolof" to R.string.wolof,
"Xhosa" to R.string.xhosa,
"Yiddish" to R.string.yiddish,
"Yoruba" to R.string.yoruba,
"Zhuang" to R.string.zhuang,
"Chinese" to R.string.chinese,
"Zulu" to R.string.zulu
)
fun getLanguageTranslationFor(language: String):Int {
return if (languageMap.containsKey(language))
languageMap[language]!!
else
-1
}
}
}

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M16,7h-1l-1,-1h-4L9,7H8C6.9,7 6,7.9 6,9v6c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V9C18,7.9 17.1,7 16,7zM12,14c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C14,13.1 13.1,14 12,14z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M8.57,0.51l4.48,4.48V2.04c4.72,0.47 8.48,4.23 8.95,8.95c0,0 2,0 2,0C23.34,3.02 15.49,-1.59 8.57,0.51z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M10.95,21.96C6.23,21.49 2.47,17.73 2,13.01c0,0 -2,0 -2,0c0.66,7.97 8.51,12.58 15.43,10.48l-4.48,-4.48V21.96z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M3.27,3L2,4.27l5,5V13h3v9l3.58,-6.14L17.73,20 19,18.73 3.27,3zM17,10h-4l4,-8H7v2.18l8.46,8.46L17,10z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,2v11h3v9l7,-12h-4l4,-8z"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M23.25,12.77l-2.57,-2.57 -1.41,1.41 2.22,2.22 -5.66,5.66L4.51,8.17l5.66,-5.66 2.1,2.1 1.41,-1.41L11.23,0.75c-0.59,-0.59 -1.54,-0.59 -2.12,0L2.75,7.11c-0.59,0.59 -0.59,1.54 0,2.12l12.02,12.02c0.59,0.59 1.54,0.59 2.12,0l6.36,-6.36c0.59,-0.59 0.59,-1.54 0,-2.12zM8.47,20.48C5.2,18.94 2.86,15.76 2.5,12L1,12c0.51,6.16 5.66,11 11.95,11l0.66,-0.03 -3.81,-3.82 -1.33,1.33zM16,9h5c0.55,0 1,-0.45 1,-1L22,4c0,-0.55 -0.45,-1 -1,-1v-0.5C21,1.12 19.88,0 18.5,0S16,1.12 16,2.5L16,3c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1zM16.8,2.5c0,-0.94 0.76,-1.7 1.7,-1.7s1.7,0.76 1.7,1.7L20.2,3h-3.4v-0.5z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M16.48,2.52c3.27,1.55 5.61,4.72 5.97,8.48h1.5C23.44,4.84 18.29,0 12,0l-0.66,0.03 3.81,3.81 1.33,-1.32zM10.23,1.75c-0.59,-0.59 -1.54,-0.59 -2.12,0L1.75,8.11c-0.59,0.59 -0.59,1.54 0,2.12l12.02,12.02c0.59,0.59 1.54,0.59 2.12,0l6.36,-6.36c0.59,-0.59 0.59,-1.54 0,-2.12L10.23,1.75zM14.83,21.19L2.81,9.17l6.36,-6.36 12.02,12.02 -6.36,6.36zM7.52,21.48C4.25,19.94 1.91,16.76 1.55,13L0.05,13C0.56,19.16 5.71,24 12,24l0.66,-0.03 -3.81,-3.81 -1.33,1.32z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M21,6.5l-4,4V7c0,-0.55 -0.45,-1 -1,-1H9.82L21,17.18V6.5zM3.27,2L2,3.27 4.73,6H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.21,0 0.39,-0.08 0.54,-0.18L19.73,21 21,19.73 3.27,2z"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63zM19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71zM4.27,3L3,4.27 7.73,9L3,9v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3zM12,4L9.91,6.09 12,8.18L12,4z"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256"
android:tint="?attr/colorControlNormal"
>
<path
android:pathData="M168,128a40,40 0,1 1,-40 -40a40.046,40.046 0,0 1,40 40zM82.745,82.745a8,8 0,1 0,-11.314 -11.313a79.94,79.94 0,0 0,0 113.136a8,8 0,1 0,11.314 -11.313a63.94,63.94 0,0 1,0 -90.51zM208,128a79.777,79.777 0,0 0,-23.43 -56.568a8,8 0,1 0,-11.315 11.313a63.94,63.94 0,0 1,0 90.51a8,8 0,0 0,11.314 11.313A79.777,79.777 0,0 0,208 128zM32.17,168.479A103.904,103.904 0,0 1,54.46 54.461a8,8 0,0 0,-11.314 -11.314a119.906,119.906 0,0 0,0 169.706a8,8 0,1 0,11.315 -11.314a103.651,103.651 0,0 1,-22.291 -33.06zM238.566,81.289a119.581,119.581 0,0 0,-25.712 -38.142a8,8 0,1 0,-11.315 11.314a103.905,103.905 0,0 1,0 147.078a8,8 0,0 0,11.315 11.314a120.121,120.121 0,0 0,25.712 -131.565z"
android:fillColor="@android:color/white"
/>
</vector>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data/>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<ProgressBar
android:id="@+id/try_connect"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"/>
<TextView
android:id="@+id/try_connect_msg"
android:layout_gravity="center"
android:text="@string/try_connect"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/error"
android:textColor="#FF0000"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/instance_title"
android:text="@string/instance"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/instance"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/error_instance"
android:textColor="#FF0000"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/instance_error"/>
<TextView
android:id="@+id/username_title"
android:text="@string/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/error_username"
android:textColor="#FF0000"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/username_error"/>
<TextView
android:id="@+id/password_title"
android:text="@string/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"/>
<TextView
android:id="@+id/error_password"
android:textColor="#FF0000"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/password_error"/>
</LinearLayout>
</layout>

View File

@ -0,0 +1,307 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data/>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="100dp"
android:clipToPadding="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<ProgressBar
android:id="@+id/loading_progress"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/loading_channels"
/>
<TextView
android:id="@+id/loading_channels"
android:gravity="center"
android:text="@string/loading_channels"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
<TextView
android:id="@+id/error"
android:textColor="#FF0000"
android:layout_gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"/>
<TextView
android:id="@+id/title"
android:text="@string/stream_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/error"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"/>
<EditText
android:id="@+id/live_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"
android:singleLine="true"/>
<TextView
android:id="@+id/title_error"
android:text="@string/stream_title_error"
android:textColor="#FF0000"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/live_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"/>
<TextView
android:id="@+id/channel"
android:text="@string/choose_channel"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/title_error"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"/>
<Spinner
android:id="@+id/channel_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/channel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"
/>
<TextView
android:id="@+id/privacy"
android:text="@string/stream_privacy"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/channel_list"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"/>
<Spinner
android:id="@+id/privacy_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/privacy"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"
/>
<TextView
android:id="@+id/advance_settings"
android:text="@string/advanced_settings"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/privacy_list"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"/>
<TextView
android:id="@+id/category"
android:text="@string/stream_category"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/advance_settings"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"/>
<Spinner
android:id="@+id/category_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/category"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"
/>
<TextView
android:id="@+id/licence"
android:text="@string/stream_licence"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/category_list"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"/>
<Spinner
android:id="@+id/licence_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/licence"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"
/>
<TextView
android:id="@+id/language"
android:text="@string/stream_language"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/licence_list"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"/>
<Spinner
android:id="@+id/language_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/language"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"
/>
<TextView
android:id="@+id/description_title"
android:text="@string/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/language_list"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"/>
<EditText
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="150dp"
app:layout_constraintTop_toBottomOf="@id/description_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"
android:inputType="textMultiLine"
android:lines="4"
android:gravity="top|start"
/>
<TextView
android:id="@+id/comments_enabled_title"
android:text="@string/comments_enabled"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/description"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/comments_enabled"
app:layout_constraintBottom_toBottomOf="@id/comments_enabled"
android:layout_margin="5dp"/>
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/comments_enabled"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/description"
app:layout_constraintStart_toEndOf="@id/comments_enabled_title"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"
/>
<TextView
android:id="@+id/download_enabled_title"
android:text="@string/download_enabled"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/comments_enabled_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/download_enabled"
app:layout_constraintBottom_toBottomOf="@id/download_enabled"
android:layout_margin="5dp"/>
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/download_enabled"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/comments_enabled_title"
app:layout_constraintStart_toEndOf="@id/download_enabled_title"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"
/>
<TextView
android:id="@+id/nsfw_title"
android:text="@string/nsfw"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/download_enabled_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/nsfw"
app:layout_constraintBottom_toBottomOf="@id/nsfw"
android:layout_margin="5dp"/>
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/nsfw"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/download_enabled_title"
app:layout_constraintStart_toEndOf="@id/nsfw_title"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"
/>
<TextView
android:id="@+id/save_replay_title"
android:text="@string/save_replay"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/nsfw_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/save_replay"
app:layout_constraintBottom_toBottomOf="@id/save_replay"
android:layout_margin="5dp"/>
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/save_replay"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/nsfw_title"
app:layout_constraintStart_toEndOf="@id/save_replay_title"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"
/>
<TextView
android:id="@+id/save_replay_info"
android:text="@string/save_replay_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/save_replay_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="5dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottom_bar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
style="@style/Widget.MaterialComponents.BottomAppBar.Colored"
android:layout_gravity="bottom"
/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/go_live"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_baseline_broadcast_24"
app:layout_anchor="@id/bottom_bar"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data/>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragment.home.HomeFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/instance_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<TextView
android:id="@+id/no_instance"
android:text="@string/no_instance"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_margin="5dp"
android:layout_width="match_parent"
android:layout_height="50dp">
<TextView
android:text="Schoumi"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/username"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/url"
android:layout_width="0dp"
android:layout_height="wrap_content"/>
<TextView
android:text="https://peertube2.cpy.re"
android:id="@+id/url"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/separator"
app:layout_constraintTop_toBottomOf="@id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/separator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
style="?android:attr/listSeparatorTextViewStyle"
android:layout_width="match_parent"
android:layout_height="5dp"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@drawable/baseline_navigate_next_24"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
android:keepScreenOn="true"
>
<TextView
android:layout_margin="5dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/permission_info"
android:text="@string/permissions"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="center"
/>
<TextView
android:layout_margin="5dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/goto_permission"
android:text="@string/goto_permissions"
app:layout_constraintTop_toBottomOf="@id/permission_info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="center"
android:textColor="#0000FF"
/>
<com.pedro.rtplibrary.view.OpenGlView
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/surfaceView"
app:keepAspectRatio="true"
app:aspectRatioMode="adjust_rotate"
app:AAEnabled="false"
app:numFilters="1"
app:isFlipHorizontal="false"
app:isFlipVertical="false"
app:layout_constraintTop_toBottomOf="@id/mute_micro"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="15dp"
/>
<ImageView
android:layout_marginTop="15dp"
android:id="@+id/rotation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/flash"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/baseline_screen_rotation_24"
app:tint="@color/white" />
<ImageView
android:layout_marginTop="15dp"
android:id="@+id/flash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/mute_micro"
app:layout_constraintStart_toEndOf="@id/rotation"
app:srcCompat="@drawable/baseline_flash_off_24"
app:tint="@color/white" />
<ImageView
android:layout_marginTop="15dp"
android:id="@+id/mute_micro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/flash"
app:layout_constraintEnd_toStartOf="@id/switch_camera"
app:srcCompat="@drawable/baseline_volume_up_24"
app:tint="@color/white" />
<ImageView
android:layout_marginTop="15dp"
android:id="@+id/switch_camera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/baseline_cameraswitch_24"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/mute_micro"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/add_instance"
android:icon="@drawable/ic_baseline_add_24"
android:title="@string/add_instance"
android:visible="true"
app:showAsAction="always" />
</menu>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -0,0 +1,289 @@
<resources>
<string name="app_name">Peertube Live</string>
<!-- errors -->
<string name="network_error">Aucune connexion internet détecté</string>
<string name="unknwon_error">Error Inconnu</string>
<string name="unknown_host">Hôte Inconnu</string>
<string name="json_error">Erreur JSON</string>
<string name="stream_title_error">Le titre ne peut pas être vide</string>
<string name="instance_error">L\'instance ne peut pas être vide</string>
<string name="username_error">Le nom d\'utilisateur ne peut pas être vide</string>
<string name="password_error">Le mot de passe ne peut pas être vide</string>
<string name="malformed_instance_error">L\'instance doit avoir une url valide</string>
<string name="account_exist">Ce compte existe déjà</string>
<!-- messages -->
<string name="no_instance">Aucun compte enregistré. Pour ajouter un compte peertube, cliquez sur le \'+\' dans la top bar</string>
<string name="loading_channels">Chargement de la liste de vos chaîne</string>
<string name="try_connect">En attente de connexion</string>
<string name="permissions">Nous avons besoin d\'accéder à la caméra et au micro pour le direct. Pour changer les paramètres de permission, cliquez en dessous</string>
<string name="delete_account">Êtes vous sure de vouloir supprimer le compte %s associé au serveur %s ?</string>
<string name="tags_rules">Maximum 5 tags, Chacun entre 2 et 30 caractères, séparé par une virgule</string>
<string name="save_replay_info">Si vous activez cette option, votre direct sera arrêté si vous dépassez votre quota vidéo</string>
<string name="back_reason">Direct terminé après que vous ayez appuyé sur la touche retour</string>
<string name="background_reason">Direct terminé car l\'application est passé à l\'arrière plan</string>
<string name="lock_reason">Direct terminé car le téléphone a été verrouillé</string>
<string name="ask_end_stream">Voulez vous arrêter le direct?</string>
<!-- buttons -->
<string name="choose_channel">Chaîne</string>
<string name="go_live">Démarrer le direct!</string>
<string name="cancel">Annuler</string>
<string name="yes">Oui</string>
<string name="no">Non</string>
<string name="goto_permissions">Aller dans les paramètres</string>
<string name="connect">Connecter</string>
<!-- titles -->
<string name="stream_title">Titre</string>
<string name="stream_category">Categorie</string>
<string name="stream_privacy">Visibilité</string>
<string name="stream_language">Langue</string>
<string name="stream_licence">Licence</string>
<string name="advanced_settings">▶ Paramètres Avancées</string>
<string name="advanced_settings_expand">▼ Paramètres Avancées</string>
<string name="add_instance">Ajouter ce compte</string>
<string name="username">Nom d\'utilisateur</string>
<string name="password">Mot de passe</string>
<string name="instance">Instance</string>
<string name="delete_account_title">Supprimer ce compte</string>
<string name="comments_enabled">Activer les commentaires</string>
<string name="download_enabled">Activer le téléchargement</string>
<string name="nsfw">Contient du contenu sensible</string>
<string name="tags">Tags</string>
<string name="description">Description</string>
<string name="save_replay">Publier une rediffusion automatiquement à la fin du direct</string>
<string name="stream_ended">Direct terminé</string>
<string name="end_stream">Arrêter le direct</string>
<!-- category -->
<string name="music">Musiques</string>
<string name="films">Films</string>
<string name="vehicles">Vehicules</string>
<string name="art">Art</string>
<string name="sports">Sports</string>
<string name="travels">Voyages</string>
<string name="gaming">Jeux Vidéos</string>
<string name="people">Personnalités</string>
<string name="comedy">Humour</string>
<string name="entertainment">Divertissement</string>
<string name="news_politics">Actualité &amp; Politique</string>
<string name="how_to">Tutoriels</string>
<string name="education">Éducation</string>
<string name="activism">Militantisme</string>
<string name="science_tech">Science &amp; Technologie</string>
<string name="animals">Animaux</string>
<string name="kids">Enfants</string>
<string name="food">Cuisine</string>
<!-- language --><string name="afar">Afar</string>
<string name="abkhazian">Abkhaze</string>
<string name="afrikaans">Afrikaans</string>
<string name="akan">Akan</string>
<string name="amharic">Amharique</string>
<string name="arabic">Arabe</string>
<string name="aragonese">Aragonais</string>
<string name="american_sign_language">Langue des signes américaine</string>
<string name="assamese">Assamais</string>
<string name="avaric">Avar</string>
<string name="kotava">Kotava</string>
<string name="aymara">Aymara</string>
<string name="azerbaijani">Azéri</string>
<string name="bashkir">Bachkir</string>
<string name="bambara">Bambara</string>
<string name="belarusian">Biélorusse</string>
<string name="bengali">Bengali</string>
<string name="british_sign_language">Langue des signes britannique</string>
<string name="bislama">Bichlamar</string>
<string name="tibetan">Tibétain</string>
<string name="bosnian">Bosniaque</string>
<string name="breton">Breton</string>
<string name="bulgarian">Bulgare</string>
<string name="brazilian_sign_language">Langue des signes brésilienne</string>
<string name="catalan">Catalan</string>
<string name="czech">Tchèque</string>
<string name="chamorro">Chamorro</string>
<string name="chechen">Tchétchène</string>
<string name="chuvash">Tchouvache</string>
<string name="cornish">Cornique</string>
<string name="corsican">Corse</string>
<string name="cree">Cree</string>
<string name="czech_sign_language">Langue des signes tchèque</string>
<string name="chinese_sign_language">Langue des signes chinoise</string>
<string name="welsh">Gallois</string>
<string name="danish">Danois</string>
<string name="german">Allemand</string>
<string name="dhivehi">Maldivien</string>
<string name="danish_sign_language">Langue des signes danoise</string>
<string name="dzongkha">Dzongkha</string>
<string name="greek">Grec</string>
<string name="english">Anglais</string>
<string name="esperanto">Espéranto</string>
<string name="estonian">Estonien</string>
<string name="basque">Basque</string>
<string name="ewe">Éwé</string>
<string name="faroese">Féroïen</string>
<string name="persian">Persan</string>
<string name="fijian">Fidjien</string>
<string name="finnish">Finnois</string>
<string name="french">Français</string>
<string name="western_frisian">Frison occidental</string>
<string name="french_sign_language">Langue des signes française</string>
<string name="fulah">Peul</string>
<string name="scottish_gaelic">Gaélique</string>
<string name="irish">Irlandais</string>
<string name="galician">Galicien</string>
<string name="manx">Manx</string>
<string name="guarani">Guarani</string>
<string name="german_sign_language">Langue des signes allemande</string>
<string name="gujarati">Goudjrati</string>
<string name="haitian">Haïtien</string>
<string name="hausa">Haoussa</string>
<string name="serbo_croatian">Serbo-croate</string>
<string name="hebrew">Hébreu</string>
<string name="herero">Herero</string>
<string name="hindi">Hindi</string>
<string name="hiri_motu">Hiri motu</string>
<string name="croatian">Croate</string>
<string name="hungarian">Hongrois</string>
<string name="armenian">Arménien</string>
<string name="igbo">Igbo</string>
<string name="sichuan_yi">Yi de Sichuan</string>
<string name="inuktitut">Inuktitut</string>
<string name="indonesian">Indonésien</string>
<string name="inupiaq">Inupiaq</string>
<string name="icelandic">Islandais</string>
<string name="italian">Italien</string>
<string name="javanese">Javanais</string>
<string name="lojban">Lojban</string>
<string name="japanese">Japonais</string>
<string name="japanese_sign_language">Langue des signes japonaise</string>
<string name="kabyle">Kabyle</string>
<string name="kalaallisut">Groenlandais</string>
<string name="kannada">Kannada</string>
<string name="kashmiri">Kashmiri</string>
<string name="georgian">Géorgien</string>
<string name="kanuri">Kanouri</string>
<string name="kazakh">Kazakh</string>
<string name="khmer">Khmer central</string>
<string name="kikuyu">Kikuyu</string>
<string name="kinyarwanda">Rwanda</string>
<string name="kirghiz">Kirghiz</string>
<string name="komi">Kom</string>
<string name="kongo">Kongo</string>
<string name="korean">Coréen</string>
<string name="kuanyama">Kuanyama</string>
<string name="kurdish">Kurde</string>
<string name="lao">Lao</string>
<string name="latvian">Letton</string>
<string name="limburgan">Limbourgeois</string>
<string name="lingala">Lingala</string>
<string name="lithuanian">Lituanien</string>
<string name="luxembourgish">Luxembourgeois</string>
<string name="luba_katanga">Luba-katanga</string>
<string name="ganda">Ganda</string>
<string name="marshallese">Marshall</string>
<string name="malayalam">Malayalam</string>
<string name="marathi">Marathe</string>
<string name="macedonian">Macédonien</string>
<string name="malagasy">Malgache</string>
<string name="maltese">Maltais</string>
<string name="mongolian">Mongol</string>
<string name="maori">Maori</string>
<string name="malay_macrolanguage">Malais</string>
<string name="burmese">Birman</string>
<string name="nauru">Nauruan</string>
<string name="navajo">Navaho</string>
<string name="south_ndebele">Ndébélé du Sud</string>
<string name="north_ndebele">Ndébélé du Nord</string>
<string name="ndonga">Ndonga</string>
<string name="nepali_macrolanguage">Népalais</string>
<string name="dutch">Néerlandais</string>
<string name="norwegian_nynorsk">Norvégien nynorsk</string>
<string name="norwegian_bokmål">Norvégien bokmål</string>
<string name="norwegian">Norvégien</string>
<string name="nyanja">Chichewa</string>
<string name="occitan">Occitane</string>
<string name="ojibwa">Ojibwa</string>
<string name="oriya_macrolanguage">Oriya</string>
<string name="oromo">Galla</string>
<string name="ossetian">Ossète</string>
<string name="panjabi">Pendjabi</string>
<string name="pakistan_sign_language">Langue des signes pakistanaise</string>
<string name="polish">Polonais</string>
<string name="portuguese">Portugais</string>
<string name="pushto">Pachto</string>
<string name="quechua">Quechua</string>
<string name="romansh">Romanche</string>
<string name="romanian">Roumain</string>
<string name="russian_sign_language">Langue des signes russe</string>
<string name="rundi">Rundi</string>
<string name="russian">Russe</string>
<string name="sango">Sango</string>
<string name="saudi_arabian_sign_language">Langue des signes saoudienne</string>
<string name="south_african_sign_language">Langue des signes sud-africaine</string>
<string name="sinhala">Singhalais</string>
<string name="slovak">Slovaque</string>
<string name="slovenian">Slovène</string>
<string name="northern_sami">Sami du Nord</string>
<string name="samoan">Samoan</string>
<string name="shona">Shona</string>
<string name="sindhi">Sindhi</string>
<string name="somali">Somali</string>
<string name="southern_sotho">Sotho du Sud</string>
<string name="spanish">Espagnol</string>
<string name="albanian">Albanais</string>
<string name="sardinian">Sarde</string>
<string name="serbian">Serbe</string>
<string name="swati">Swati</string>
<string name="sundanese">Soundanais</string>
<string name="swahili_macrolanguage">Swahili</string>
<string name="swedish">Suédois</string>
<string name="swedish_sign_language">Langue des signes suédoise</string>
<string name="tahitian">Tahitien</string>
<string name="tamil">Tamoul</string>
<string name="tatar">Tatar</string>
<string name="telugu">Télougou</string>
<string name="tajik">Tadjik</string>
<string name="tagalog">Tagalog</string>
<string name="thai">Thaï</string>
<string name="tigrinya">Tigrigna</string>
<string name="klingon">Klingon</string>
<string name="tonga_tonga_islands">Tongan (Îles Tonga)</string>
<string name="tswana">Tswana</string>
<string name="tsonga">Tsonga</string>
<string name="turkmen">Turkmène</string>
<string name="turkish">Turc</string>
<string name="twi">Twi</string>
<string name="uighur">Ouïgour</string>
<string name="ukrainian">Ukrainien</string>
<string name="urdu">Ourdou</string>
<string name="uzbek">Ouszbek</string>
<string name="venda">Venda</string>
<string name="vietnamese">Vietnamien</string>
<string name="walloon">Wallon</string>
<string name="wolof">Wolof</string>
<string name="xhosa">Xhosa</string>
<string name="yiddish">Yiddish</string>
<string name="yoruba">Yoruba</string>
<string name="zhuang">Zhuang</string>
<string name="chinese">Chinois</string>
<string name="zulu">Zoulou</string>
<!-- licence -->
<string name="by">Attribution</string>>
<string name="bysa">Attribution - Partage dans les mêmes conditions</string>
<string name="bynd">Attribution - Pas d\'œuvre dérivée</string>
<string name="bync">Attribution - Utilisation non commerciale</string>
<string name="byncsa">Attribution - Utilisation non commerciale - Partage dans les mêmes conditions</string>
<string name="byncnd">Attribution - Utilisation non commerciale - Pas d\'œuvre dérivée</string>
<string name="public_domain">Domaine Publique</string>
<!-- privacies -->
<string name="privacy_public">Publique</string>
<string name="unlisted">Non Listée</string>
<string name="privacy_private">Privée</string>
<string name="internal">Interne</string>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#202020</color>
<color name="colorPrimaryDark">#202020</color>
<color name="colorSecondary">#F2690D</color>
<color name="colorOnSecondary">#000000</color>
<color name="white">#FFFFFF</color>
</resources>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#F2690D</color>
<color name="colorPrimaryDark">#B83900</color>
<color name="colorSecondary">#202020</color>
<color name="colorOnSecondary">#FFFFFF</color>
<color name="white">#FFFFFF</color>
</resources>
<!--
<resources>
<color name="colorPrimary">#202020</color>
<color name="colorPrimaryDark">#111111</color>
<color name="colorSecondary">#F2690D</color>
<color name="colorOnSecondary">#000000</color>
<color name="white">#FFFFFF</color>
</resources>
-->

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@ -0,0 +1,289 @@
<resources>
<string name="app_name">Peertube Live</string>
<!-- errors -->
<string name="network_error">No Internet Connection Found</string>
<string name="unknwon_error">Unknown Error</string>
<string name="unknown_host">Unknown Host</string>
<string name="json_error">JSON Error</string>
<string name="stream_title_error">Title should not be empty</string>
<string name="instance_error">Instance cannot be empty</string>
<string name="username_error">Username cannot be empty</string>
<string name="password_error">Password cannot be empty</string>
<string name="malformed_instance_error">Instance should be a valid url</string>
<string name="account_exist">This account already exist</string>
<!-- messages -->
<string name="no_instance">No account registered. To add a peertube account, click on the \'+\' in the top bar</string>
<string name="loading_channels">Loading your channel list</string>
<string name="try_connect">Waiting for connection</string>
<string name="permissions">We need the access to the camera and microphone to live stream. To change permissions settings click below</string>
<string name="delete_account">Are you sure you want to delete the account %s associated with the server %s ?</string>
<string name="tags_rules">Maximum 5 tags, each between 2 and 30 characters, separate by comma</string>
<string name="save_replay_info">If you enable this option, your live will be terminated if you exceed your video quota</string>
<string name="back_reason">Live has stop after you pressed back button</string>
<string name="background_reason">Live has stop because the app has gone to background</string>
<string name="lock_reason">Live has stop because the phone was locked</string>
<string name="ask_end_stream">Do you want to stop the live?</string>
<!-- buttons -->
<string name="choose_channel">Channel</string>
<string name="go_live">Go Live!</string>
<string name="cancel">Cancel</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="goto_permissions">View settings</string>
<string name="connect">Connect</string>
<!-- titles -->
<string name="stream_title">Title</string>
<string name="stream_category">Category</string>
<string name="stream_privacy">Privacy</string>
<string name="stream_language">Language</string>
<string name="stream_licence">Licence</string>
<string name="advanced_settings">▶ Advanced Settings</string>
<string name="advanced_settings_expand">▼ Advanced Settings</string>
<string name="add_instance">Add this account</string>
<string name="username">Username</string>
<string name="password">Password</string>
<string name="instance">Instance</string>
<string name="delete_account_title">Delete this account</string>
<string name="comments_enabled">Enable video comments</string>
<string name="download_enabled">Enable download</string>
<string name="nsfw">Contains sensitive content</string>
<string name="tags">Tags</string>
<string name="description">Description</string>
<string name="save_replay">Automatically publish a replay when your live ends</string>
<string name="stream_ended">Live ended</string>
<string name="end_stream">Stop the live</string>
<!-- category -->
<string name="music">Music</string>
<string name="films">Films</string>
<string name="vehicles">Vehicles</string>
<string name="art">Art</string>
<string name="sports">Sports</string>
<string name="travels">Travels</string>
<string name="gaming">Gaming</string>
<string name="people">People</string>
<string name="comedy">Comedy</string>
<string name="entertainment">Entertainment</string>
<string name="news_politics">News &amp; Politics</string>
<string name="how_to">How To</string>
<string name="education">Education</string>
<string name="activism">Activism</string>
<string name="science_tech">Science &amp; Technology</string>
<string name="animals">Animals</string>
<string name="kids">Kids</string>
<string name="food">Food</string>
<!-- language -->
<string name="afar">Afar</string>
<string name="abkhazian">Abkhazian</string>
<string name="afrikaans">Afrikaans</string>
<string name="akan">Akan</string>
<string name="amharic">Amharic</string>
<string name="arabic">Arabic</string>
<string name="aragonese">Aragonese</string>
<string name="american_sign_language">American Sign Language</string>
<string name="assamese">Assamese</string>
<string name="avaric">Avaric</string>
<string name="kotava">Kotava</string>
<string name="aymara">Aymara</string>
<string name="azerbaijani">Azerbaijani</string>
<string name="bashkir">Bashkir</string>
<string name="bambara">Bambara</string>
<string name="belarusian">Belarusian</string>
<string name="bengali">Bengali</string>
<string name="british_sign_language">British Sign Language</string>
<string name="bislama">Bislama</string>
<string name="tibetan">Tibetan</string>
<string name="bosnian">Bosnian</string>
<string name="breton">Breton</string>
<string name="bulgarian">Bulgarian</string>
<string name="brazilian_sign_language">Brazilian Sign Language</string>
<string name="catalan">Catalan</string>
<string name="czech">Czech</string>
<string name="chamorro">Chamorro</string>
<string name="chechen">Chechen</string>
<string name="chuvash">Chuvash</string>
<string name="cornish">Cornish</string>
<string name="corsican">Corsican</string>
<string name="cree">Cree</string>
<string name="czech_sign_language">Czech Sign Language</string>
<string name="chinese_sign_language">Chinese Sign Language</string>
<string name="welsh">Welsh</string>
<string name="danish">Danish</string>
<string name="german">German</string>
<string name="dhivehi">Dhivehi</string>
<string name="danish_sign_language">Danish Sign Language</string>
<string name="dzongkha">Dzongkha</string>
<string name="greek">Greek</string>
<string name="english">English</string>
<string name="esperanto">Esperanto</string>
<string name="estonian">Estonian</string>
<string name="basque">Basque</string>
<string name="ewe">Ewe</string>
<string name="faroese">Faroese</string>
<string name="persian">Persian</string>
<string name="fijian">Fijian</string>
<string name="finnish">Finnish</string>
<string name="french">French</string>
<string name="western_frisian">Western Frisian</string>
<string name="french_sign_language">French Sign Language</string>
<string name="fulah">Fulah</string>
<string name="scottish_gaelic">Scottish Gaelic</string>
<string name="irish">Irish</string>
<string name="galician">Galician</string>
<string name="manx">Manx</string>
<string name="guarani">Guarani</string>
<string name="german_sign_language">German Sign Language</string>
<string name="gujarati">Gujarati</string>
<string name="haitian">Haitian</string>
<string name="hausa">Hausa</string>
<string name="serbo_croatian">Serbo-Croatian</string>
<string name="hebrew">Hebrew</string>
<string name="herero">Herero</string>
<string name="hindi">Hindi</string>
<string name="hiri_motu">Hiri Motu</string>
<string name="croatian">Croatian</string>
<string name="hungarian">Hungarian</string>
<string name="armenian">Armenian</string>
<string name="igbo">Igbo</string>
<string name="sichuan_yi">Sichuan Yi</string>
<string name="inuktitut">Inuktitut</string>
<string name="indonesian">Indonesian</string>
<string name="inupiaq">Inupiaq</string>
<string name="icelandic">Icelandic</string>
<string name="italian">Italian</string>
<string name="javanese">Javanese</string>
<string name="lojban">Lojban</string>
<string name="japanese">Japanese</string>
<string name="japanese_sign_language">Japanese Sign Language</string>
<string name="kabyle">Kabyle</string>
<string name="kalaallisut">Kalaallisut</string>
<string name="kannada">Kannada</string>
<string name="kashmiri">Kashmiri</string>
<string name="georgian">Georgian</string>
<string name="kanuri">Kanuri</string>
<string name="kazakh">Kazakh</string>
<string name="khmer">Khmer</string>
<string name="kikuyu">Kikuyu</string>
<string name="kinyarwanda">Kinyarwanda</string>
<string name="kirghiz">Kirghiz</string>
<string name="komi">Komi</string>
<string name="kongo">Kongo</string>
<string name="korean">Korean</string>
<string name="kuanyama">Kuanyama</string>
<string name="kurdish">Kurdish</string>
<string name="lao">Lao</string>
<string name="latvian">Latvian</string>
<string name="limburgan">Limburgan</string>
<string name="lingala">Lingala</string>
<string name="lithuanian">Lithuanian</string>
<string name="luxembourgish">Luxembourgish</string>
<string name="luba_katanga">Luba-Katanga</string>
<string name="ganda">Ganda</string>
<string name="marshallese">Marshallese</string>
<string name="malayalam">Malayalam</string>
<string name="marathi">Marathi</string>
<string name="macedonian">Macedonian</string>
<string name="malagasy">Malagasy</string>
<string name="maltese">Maltese</string>
<string name="mongolian">Mongolian</string>
<string name="maori">Maori</string>
<string name="malay_macrolanguage">Malay (macrolanguage)</string>
<string name="burmese">Burmese</string>
<string name="nauru">Nauru</string>
<string name="navajo">Navajo</string>
<string name="south_ndebele">South Ndebele</string>
<string name="north_ndebele">North Ndebele</string>
<string name="ndonga">Ndonga</string>
<string name="nepali_macrolanguage">Nepali (macrolanguage)</string>
<string name="dutch">Dutch</string>
<string name="norwegian_nynorsk">Norwegian Nynorsk</string>
<string name="norwegian_bokmål">Norwegian Bokmål</string>
<string name="norwegian">Norwegian</string>
<string name="nyanja">Nyanja</string>
<string name="occitan">Occitan</string>
<string name="ojibwa">Ojibwa</string>
<string name="oriya_macrolanguage">Oriya (macrolanguage)</string>
<string name="oromo">Oromo</string>
<string name="ossetian">Ossetian</string>
<string name="panjabi">Panjabi</string>
<string name="pakistan_sign_language">Pakistan Sign Language</string>
<string name="polish">Polish</string>
<string name="portuguese">Portuguese</string>
<string name="pushto">Pushto</string>
<string name="quechua">Quechua</string>
<string name="romansh">Romansh</string>
<string name="romanian">Romanian</string>
<string name="russian_sign_language">Russian Sign Language</string>
<string name="rundi">Rundi</string>
<string name="russian">Russian</string>
<string name="sango">Sango</string>
<string name="saudi_arabian_sign_language">Saudi Arabian Sign Language</string>
<string name="south_african_sign_language">South African Sign Language</string>
<string name="sinhala">Sinhala</string>
<string name="slovak">Slovak</string>
<string name="slovenian">Slovenian</string>
<string name="northern_sami">Northern Sami</string>
<string name="samoan">Samoan</string>
<string name="shona">Shona</string>
<string name="sindhi">Sindhi</string>
<string name="somali">Somali</string>
<string name="southern_sotho">Southern Sotho</string>
<string name="spanish">Spanish</string>
<string name="albanian">Albanian</string>
<string name="sardinian">Sardinian</string>
<string name="serbian">Serbian</string>
<string name="swati">Swati</string>
<string name="sundanese">Sundanese</string>
<string name="swahili_macrolanguage">Swahili (macrolanguage)</string>
<string name="swedish">Swedish</string>
<string name="swedish_sign_language">Swedish Sign Language</string>
<string name="tahitian">Tahitian</string>
<string name="tamil">Tamil</string>
<string name="tatar">Tatar</string>
<string name="telugu">Telugu</string>
<string name="tajik">Tajik</string>
<string name="tagalog">Tagalog</string>
<string name="thai">Thai</string>
<string name="tigrinya">Tigrinya</string>
<string name="klingon">Klingon</string>
<string name="tonga_tonga_islands">Tonga (Tonga Islands)</string>
<string name="tswana">Tswana</string>
<string name="tsonga">Tsonga</string>
<string name="turkmen">Turkmen</string>
<string name="turkish">Turkish</string>
<string name="twi">Twi</string>
<string name="uighur">Uighur</string>
<string name="ukrainian">Ukrainian</string>
<string name="urdu">Urdu</string>
<string name="uzbek">Uzbek</string>
<string name="venda">Venda</string>
<string name="vietnamese">Vietnamese</string>
<string name="walloon">Walloon</string>
<string name="wolof">Wolof</string>
<string name="xhosa">Xhosa</string>
<string name="yiddish">Yiddish</string>
<string name="yoruba">Yoruba</string>
<string name="zhuang">Zhuang</string>
<string name="chinese">Chinese</string>
<string name="zulu">Zulu</string>
<!-- licence -->
<string name="by">Attribution</string>>
<string name="bysa">Attribution - Share Alike</string>
<string name="bynd">Attribution - No Derivatives</string>
<string name="bync">Attribution - Non Commercial</string>
<string name="byncsa">Attribution - Non Commercial - Share Alike</string>
<string name="byncnd">Attribution - Non Commercial - No Derivatives</string>
<string name="public_domain">Public Domain Dedication</string>
<!-- privacies -->
<string name="privacy_public">Public</string>
<string name="unlisted">Unlisted</string>
<string name="privacy_private">Private</string>
<string name="internal">Internal</string>
</resources>

View File

@ -0,0 +1,22 @@
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorSecondary">@color/colorSecondary</item>
<item name="colorOnSecondary">@color/colorOnSecondary</item>
<item name="alertDialogTheme">@style/DialogButton</item>
<!--<item name="android:editTextColor">@color/colorAccent</item>-->
</style>
<style name="DialogButton" parent="Theme.AppCompat.DayNight.Dialog.Alert">
<item name="buttonBarNegativeButtonStyle">@style/buttonStyle</item>
<item name="buttonBarPositiveButtonStyle">@style/buttonStyle</item>
<item name="buttonBarNeutralButtonStyle">@style/buttonStyle</item>
</style>
<style name="buttonStyle" parent="Widget.AppCompat.Button.ButtonBar.AlertDialog">
<item name="android:textColor">@color/colorSecondary</item>
</style>
</resources>

View File

@ -0,0 +1,17 @@
package org.framasoft.peertubelive
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)
}
}

28
build.gradle Normal file
View File

@ -0,0 +1,28 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.5.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

1
encoder/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

22
encoder/build.gradle Normal file
View File

@ -0,0 +1,22 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion 30
defaultConfig {
minSdkVersion 16
targetSdkVersion 30
versionCode 182
versionName "1.8.2"
}
buildTypes {
release {
minifyEnabled false
consumerProguardFiles 'proguard-rules.pro'
}
}
}
dependencies {
api 'androidx.annotation:annotation:1.2.0'
}

17
encoder/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/pedro/Android/Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# 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 *;
#}

View File

@ -0,0 +1 @@
<manifest package="com.pedro.encoder" />

View File

@ -0,0 +1,125 @@
package com.pedro.encoder;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import androidx.annotation.NonNull;
import com.pedro.encoder.utils.CodecUtil;
import java.nio.ByteBuffer;
/**
* Created by pedro on 18/09/19.
*/
public abstract class BaseEncoder implements EncoderCallback {
private static final String TAG = "BaseEncoder";
private MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
protected MediaCodec codec;
protected long presentTimeUs;
protected volatile boolean running = false;
protected boolean isBufferMode = true;
protected CodecUtil.Force force = CodecUtil.Force.FIRST_COMPATIBLE_FOUND;
public void start() {
start(true);
}
public abstract void start(boolean resetTs);
protected abstract void stopImp();
public void stop() {
running = false;
stopImp();
try {
codec.stop();
codec.release();
codec = null;
} catch (IllegalStateException | NullPointerException e) {
codec = null;
}
}
protected abstract MediaCodecInfo chooseEncoder(String mime);
protected void getDataFromEncoder(Frame frame) throws IllegalStateException {
if (isBufferMode) {
int inBufferIndex = codec.dequeueInputBuffer(0);
if (inBufferIndex >= 0) {
inputAvailable(codec, inBufferIndex, frame);
}
}
for (; running; ) {
int outBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0);
if (outBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat mediaFormat = codec.getOutputFormat();
formatChanged(codec, mediaFormat);
} else if (outBufferIndex >= 0) {
outputAvailable(codec, outBufferIndex, bufferInfo);
} else {
break;
}
}
}
protected abstract Frame getInputFrame() throws InterruptedException;
private void processInput(@NonNull ByteBuffer byteBuffer, @NonNull MediaCodec mediaCodec,
int inBufferIndex, Frame frame) throws IllegalStateException {
try {
if (frame == null) frame = getInputFrame();
byteBuffer.clear();
byteBuffer.put(frame.getBuffer(), frame.getOffset(), frame.getSize());
long pts = System.nanoTime() / 1000 - presentTimeUs;
mediaCodec.queueInputBuffer(inBufferIndex, 0, frame.getSize(), pts, 0);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
protected abstract void checkBuffer(@NonNull ByteBuffer byteBuffer,
@NonNull MediaCodec.BufferInfo bufferInfo);
protected abstract void sendBuffer(@NonNull ByteBuffer byteBuffer,
@NonNull MediaCodec.BufferInfo bufferInfo);
private void processOutput(@NonNull ByteBuffer byteBuffer, @NonNull MediaCodec mediaCodec,
int outBufferIndex, @NonNull MediaCodec.BufferInfo bufferInfo) throws IllegalStateException {
checkBuffer(byteBuffer, bufferInfo);
sendBuffer(byteBuffer, bufferInfo);
mediaCodec.releaseOutputBuffer(outBufferIndex, false);
}
public void setForce(CodecUtil.Force force) {
this.force = force;
}
public boolean isRunning() {
return running;
}
@Override
public void inputAvailable(@NonNull MediaCodec mediaCodec, int inBufferIndex, Frame frame)
throws IllegalStateException {
ByteBuffer byteBuffer;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
byteBuffer = mediaCodec.getInputBuffer(inBufferIndex);
} else {
byteBuffer = mediaCodec.getInputBuffers()[inBufferIndex];
}
processInput(byteBuffer, mediaCodec, inBufferIndex, frame);
}
@Override
public void outputAvailable(@NonNull MediaCodec mediaCodec, int outBufferIndex,
@NonNull MediaCodec.BufferInfo bufferInfo) throws IllegalStateException {
ByteBuffer byteBuffer;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
byteBuffer = mediaCodec.getOutputBuffer(outBufferIndex);
} else {
byteBuffer = mediaCodec.getOutputBuffers()[outBufferIndex];
}
processOutput(byteBuffer, mediaCodec, outBufferIndex, bufferInfo);
}
}

View File

@ -0,0 +1,18 @@
package com.pedro.encoder;
import android.media.MediaCodec;
import android.media.MediaFormat;
import androidx.annotation.NonNull;
/**
* Created by pedro on 18/09/19.
*/
public interface EncoderCallback {
void inputAvailable(@NonNull MediaCodec mediaCodec, int inBufferIndex, Frame frame)
throws IllegalStateException;
void outputAvailable(@NonNull MediaCodec mediaCodec, int outBufferIndex,
@NonNull MediaCodec.BufferInfo bufferInfo) throws IllegalStateException;
void formatChanged(@NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat);
}

View File

@ -0,0 +1,86 @@
package com.pedro.encoder;
import android.graphics.ImageFormat;
/**
* Created by pedro on 17/02/18.
*/
public class Frame {
private byte[] buffer;
private int offset;
private int size;
private int orientation;
private boolean flip;
private int format = ImageFormat.NV21; //nv21 or yv12 supported
/**
* Used with video frame
*/
public Frame(byte[] buffer, int orientation, boolean flip, int format) {
this.buffer = buffer;
this.orientation = orientation;
this.flip = flip;
this.format = format;
offset = 0;
size = buffer.length;
}
/**
* Used with audio frame
*/
public Frame(byte[] buffer, int offset, int size) {
this.buffer = buffer;
this.offset = offset;
this.size = size;
}
public byte[] getBuffer() {
return buffer;
}
public void setBuffer(byte[] buffer) {
this.buffer = buffer;
}
public int getOrientation() {
return orientation;
}
public void setOrientation(int orientation) {
this.orientation = orientation;
}
public boolean isFlip() {
return flip;
}
public void setFlip(boolean flip) {
this.flip = flip;
}
public int getFormat() {
return format;
}
public void setFormat(int format) {
this.format = format;
}
public int getOffset() {
return offset;
}
public void setOffset(int offset) {
this.offset = offset;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
}

View File

@ -0,0 +1,163 @@
package com.pedro.encoder.audio;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.util.Log;
import androidx.annotation.NonNull;
import com.pedro.encoder.BaseEncoder;
import com.pedro.encoder.Frame;
import com.pedro.encoder.input.audio.GetMicrophoneData;
import com.pedro.encoder.utils.CodecUtil;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Created by pedro on 19/01/17.
*
* Encode PCM audio data to ACC and return in a callback
*/
public class AudioEncoder extends BaseEncoder implements GetMicrophoneData {
private static final String TAG = "AudioEncoder";
private GetAacData getAacData;
private int bitRate = 64 * 1024; //in kbps
private int sampleRate = 32000; //in hz
private boolean isStereo = true;
public AudioEncoder(GetAacData getAacData) {
this.getAacData = getAacData;
}
/**
* Prepare encoder with custom parameters
*/
public boolean prepareAudioEncoder(int bitRate, int sampleRate, boolean isStereo,
int maxInputSize) {
this.sampleRate = sampleRate;
isBufferMode = true;
try {
List<MediaCodecInfo> encoders = new ArrayList<>();
if (force == CodecUtil.Force.HARDWARE) {
encoders = CodecUtil.getAllHardwareEncoders(CodecUtil.AAC_MIME);
} else if (force == CodecUtil.Force.SOFTWARE) {
encoders = CodecUtil.getAllSoftwareEncoders(CodecUtil.AAC_MIME);
}
if (force == CodecUtil.Force.FIRST_COMPATIBLE_FOUND) {
MediaCodecInfo encoder = chooseEncoder(CodecUtil.AAC_MIME);
if (encoder != null) {
codec = MediaCodec.createByCodecName(encoder.getName());
} else {
Log.e(TAG, "Valid encoder not found");
return false;
}
} else {
if (encoders.isEmpty()) {
Log.e(TAG, "Valid encoder not found");
return false;
} else {
codec = MediaCodec.createByCodecName(encoders.get(0).getName());
}
}
int channelCount = (isStereo) ? 2 : 1;
MediaFormat audioFormat =
MediaFormat.createAudioFormat(CodecUtil.AAC_MIME, sampleRate, channelCount);
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC);
codec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
running = false;
Log.i(TAG, "prepared");
return true;
} catch (IOException | IllegalStateException e) {
Log.e(TAG, "Create AudioEncoder failed.", e);
return false;
}
}
/**
* Prepare encoder with default parameters
*/
public boolean prepareAudioEncoder() {
return prepareAudioEncoder(bitRate, sampleRate, isStereo, 0);
}
@Override
public void start(boolean resetTs) {
presentTimeUs = System.nanoTime() / 1000;
codec.start();
running = true;
Log.i(TAG, "started");
}
@Override
protected void stopImp() {
Log.i(TAG, "stopped");
}
@Override
protected Frame getInputFrame() throws InterruptedException {
return null;
}
@Override
protected void checkBuffer(@NonNull ByteBuffer byteBuffer,
@NonNull MediaCodec.BufferInfo bufferInfo) {
}
@Override
protected void sendBuffer(@NonNull ByteBuffer byteBuffer,
@NonNull MediaCodec.BufferInfo bufferInfo) {
getAacData.getAacData(byteBuffer, bufferInfo);
}
/**
* Set custom PCM data.
* Use it after prepareAudioEncoder(int sampleRate, int channel).
* Used too with microphone.
*
*/
@Override
public void inputPCMData(Frame frame) {
if (running) {
try {
getDataFromEncoder(frame);
} catch (IllegalStateException e) {
Log.i(TAG, "Encoding error", e);
}
} else {
Log.i(TAG, "frame discarded");
}
}
@Override
protected MediaCodecInfo chooseEncoder(String mime) {
List<MediaCodecInfo> mediaCodecInfoList = CodecUtil.getAllEncoders(mime);
for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
String name = mediaCodecInfo.getName().toLowerCase();
if (!name.contains("omx.google")) return mediaCodecInfo;
}
if (mediaCodecInfoList.size() > 0) {
return mediaCodecInfoList.get(0);
} else {
return null;
}
}
public void setSampleRate(int sampleRate) {
this.sampleRate = sampleRate;
}
@Override
public void formatChanged(@NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) {
getAacData.onAudioFormat(mediaFormat);
}
}

View File

@ -0,0 +1,17 @@
package com.pedro.encoder.audio;
import android.media.MediaCodec;
import android.media.MediaFormat;
import java.nio.ByteBuffer;
/**
* Created by pedro on 19/01/17.
*/
public interface GetAacData {
void getAacData(ByteBuffer aacBuffer, MediaCodec.BufferInfo info);
void onAudioFormat(MediaFormat mediaFormat);
}

View File

@ -0,0 +1,78 @@
package com.pedro.encoder.input.audio;
import android.media.audiofx.AcousticEchoCanceler;
import android.media.audiofx.AutomaticGainControl;
import android.media.audiofx.NoiseSuppressor;
import android.util.Log;
/**
* Created by pedro on 11/05/17.
*/
public class AudioPostProcessEffect {
private final String TAG = "AudioPostProcessEffect";
private int microphoneId;
private AcousticEchoCanceler acousticEchoCanceler = null;
private AutomaticGainControl automaticGainControl = null;
private NoiseSuppressor noiseSuppressor = null;
public AudioPostProcessEffect(int microphoneId) {
this.microphoneId = microphoneId;
}
public void enableAutoGainControl() {
if (AutomaticGainControl.isAvailable() && automaticGainControl == null) {
automaticGainControl = AutomaticGainControl.create(microphoneId);
automaticGainControl.setEnabled(true);
Log.i(TAG, "AutoGainControl enabled");
} else {
Log.e(TAG, "This device don't support AutoGainControl");
}
}
public void releaseAutoGainControl() {
if (automaticGainControl != null) {
automaticGainControl.setEnabled(false);
automaticGainControl.release();
automaticGainControl = null;
}
}
public void enableEchoCanceler() {
if (AcousticEchoCanceler.isAvailable() && acousticEchoCanceler == null) {
acousticEchoCanceler = AcousticEchoCanceler.create(microphoneId);
acousticEchoCanceler.setEnabled(true);
Log.i(TAG, "EchoCanceler enabled");
} else {
Log.e(TAG, "This device don't support EchoCanceler");
}
}
public void releaseEchoCanceler() {
if (acousticEchoCanceler != null) {
acousticEchoCanceler.setEnabled(false);
acousticEchoCanceler.release();
acousticEchoCanceler = null;
}
}
public void enableNoiseSuppressor() {
if (NoiseSuppressor.isAvailable() && noiseSuppressor == null) {
noiseSuppressor = NoiseSuppressor.create(microphoneId);
noiseSuppressor.setEnabled(true);
Log.i(TAG, "NoiseSuppressor enabled");
} else {
Log.e(TAG, "This device don't support NoiseSuppressor");
}
}
public void releaseNoiseSuppressor() {
if (noiseSuppressor != null) {
noiseSuppressor.setEnabled(false);
noiseSuppressor.release();
noiseSuppressor = null;
}
}
}

View File

@ -0,0 +1,10 @@
package com.pedro.encoder.input.audio;
public abstract class CustomAudioEffect {
/**
* @param pcmBuffer buffer obtained directly from the microphone.
* @return it must be of same size that pcmBuffer parameter.
*/
public abstract byte[] process(byte[] pcmBuffer);
}

View File

@ -0,0 +1,12 @@
package com.pedro.encoder.input.audio;
import com.pedro.encoder.Frame;
/**
* Created by pedro on 19/01/17.
*/
public interface GetMicrophoneData {
void inputPCMData(Frame frame);
}

View File

@ -0,0 +1,235 @@
package com.pedro.encoder.input.audio;
import android.media.AudioFormat;
import android.media.AudioPlaybackCaptureConfiguration;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.media.projection.MediaProjection;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import com.pedro.encoder.Frame;
import java.nio.ByteBuffer;
/**
* Created by pedro on 19/01/17.
*/
public class MicrophoneManager {
private final String TAG = "MicrophoneManager";
private static final int BUFFER_SIZE = 4096;
protected AudioRecord audioRecord;
private GetMicrophoneData getMicrophoneData;
private ByteBuffer pcmBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
private byte[] pcmBufferMuted = new byte[BUFFER_SIZE];
protected boolean running = false;
private boolean created = false;
//default parameters for microphone
private int sampleRate = 32000; //hz
private int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
private int channel = AudioFormat.CHANNEL_IN_STEREO;
private boolean muted = false;
private AudioPostProcessEffect audioPostProcessEffect;
HandlerThread handlerThread;
private CustomAudioEffect customAudioEffect = new NoAudioEffect();
public MicrophoneManager(GetMicrophoneData getMicrophoneData) {
this.getMicrophoneData = getMicrophoneData;
}
public void setCustomAudioEffect(CustomAudioEffect customAudioEffect) {
this.customAudioEffect = customAudioEffect;
}
/**
* Create audio record
*/
public void createMicrophone() {
createMicrophone(sampleRate, true, false, false);
Log.i(TAG, "Microphone created, " + sampleRate + "hz, Stereo");
}
/**
* Create audio record with params and default audio source
*/
public void createMicrophone(int sampleRate, boolean isStereo, boolean echoCanceler,
boolean noiseSuppressor) {
createMicrophone(MediaRecorder.AudioSource.DEFAULT, sampleRate, isStereo, echoCanceler, noiseSuppressor);
}
/**
* Create audio record with params and selected audio source
* @param audioSource - the recording source. See {@link MediaRecorder.AudioSource} for the recording source definitions.
*/
public void createMicrophone(int audioSource, int sampleRate, boolean isStereo, boolean echoCanceler,
boolean noiseSuppressor) {
this.sampleRate = sampleRate;
if (!isStereo) channel = AudioFormat.CHANNEL_IN_MONO;
audioRecord =
new AudioRecord(audioSource, sampleRate, channel, audioFormat,
getPcmBufferSize());
audioPostProcessEffect = new AudioPostProcessEffect(audioRecord.getAudioSessionId());
if (echoCanceler) audioPostProcessEffect.enableEchoCanceler();
if (noiseSuppressor) audioPostProcessEffect.enableNoiseSuppressor();
String chl = (isStereo) ? "Stereo" : "Mono";
Log.i(TAG, "Microphone created, " + sampleRate + "hz, " + chl);
created = true;
}
/**
* Create audio record with params and AudioPlaybackCaptureConfig used for capturing internal audio
* Notice that you should granted {@link android.Manifest.permission#RECORD_AUDIO} before calling this!
*
* @param config - AudioPlaybackCaptureConfiguration received from {@link android.media.projection.MediaProjection}
*
* @see AudioPlaybackCaptureConfiguration.Builder#Builder(MediaProjection)
* @see "https://developer.android.com/guide/topics/media/playback-capture"
* @see "https://medium.com/@debuggingisfun/android-10-audio-capture-77dd8e9070f9"
*/
public void createInternalMicrophone(AudioPlaybackCaptureConfiguration config, int sampleRate, boolean isStereo) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
this.sampleRate = sampleRate;
if (!isStereo) channel = AudioFormat.CHANNEL_IN_MONO;
audioRecord = new AudioRecord.Builder()
.setAudioPlaybackCaptureConfig(config)
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(audioFormat)
.setSampleRate(sampleRate)
.setChannelMask(channel)
.build())
.setBufferSizeInBytes(getPcmBufferSize())
.build();
audioPostProcessEffect = new AudioPostProcessEffect(audioRecord.getAudioSessionId());
String chl = (isStereo) ? "Stereo" : "Mono";
Log.i(TAG, "Internal microphone created, " + sampleRate + "hz, " + chl);
created = true;
} else createMicrophone(sampleRate, isStereo, false, false);
}
/**
* Start record and get data
*/
public synchronized void start() {
init();
handlerThread = new HandlerThread(TAG);
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
handler.post(new Runnable() {
@Override
public void run() {
while (running) {
Frame frame = read();
if (frame != null) {
getMicrophoneData.inputPCMData(frame);
} else {
running = false;
}
}
}
});
}
private void init() {
if (audioRecord != null) {
audioRecord.startRecording();
running = true;
Log.i(TAG, "Microphone started");
} else {
Log.e(TAG, "Error starting, microphone was stopped or not created, "
+ "use createMicrophone() before start()");
}
}
public void mute() {
muted = true;
}
public void unMute() {
muted = false;
}
public boolean isMuted() {
return muted;
}
/**
* @return Object with size and PCM buffer data
*/
private Frame read() {
pcmBuffer.rewind();
int size = audioRecord.read(pcmBuffer, pcmBuffer.remaining());
if (size <= 0) {
return null;
}
return new Frame(muted ? pcmBufferMuted : customAudioEffect.process(pcmBuffer.array()),
muted ? 0 : pcmBuffer.arrayOffset(), size);
}
/**
* Stop and release microphone
*/
public synchronized void stop() {
running = false;
created = false;
if (handlerThread != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
handlerThread.quitSafely();
} else {
handlerThread.quit();
}
}
if (audioRecord != null) {
audioRecord.setRecordPositionUpdateListener(null);
audioRecord.stop();
audioRecord.release();
audioRecord = null;
}
if (audioPostProcessEffect != null) {
audioPostProcessEffect.releaseEchoCanceler();
audioPostProcessEffect.releaseNoiseSuppressor();
}
Log.i(TAG, "Microphone stopped");
}
/**
* Get PCM buffer size
*/
private int getPcmBufferSize() {
int pcmBufSize =
AudioRecord.getMinBufferSize(sampleRate, channel, AudioFormat.ENCODING_PCM_16BIT);
return pcmBufSize * 5;
}
public int getMaxInputSize() {
return BUFFER_SIZE;
}
public int getSampleRate() {
return sampleRate;
}
public void setSampleRate(int sampleRate) {
this.sampleRate = sampleRate;
}
public int getAudioFormat() {
return audioFormat;
}
public int getChannel() {
return channel;
}
public boolean isRunning() {
return running;
}
public boolean isCreated() {
return created;
}
}

View File

@ -0,0 +1,57 @@
package com.pedro.encoder.input.audio;
import android.os.HandlerThread;
import android.util.Log;
import java.nio.ByteBuffer;
/**
* Similar to MicrophoneManager but samples are not read automatically.
* The owner must manually call read(...) as often as samples are needed.
*/
public class MicrophoneManagerManual extends MicrophoneManager {
private final String TAG = "MicMM";
public MicrophoneManagerManual() {
super(null);
}
/**
* Start record and get data
*/
@Override
public synchronized void start() {
init();
}
private void init() {
if (audioRecord != null) {
audioRecord.startRecording();
running = true;
Log.i(TAG, "Microphone started");
} else {
Log.e(TAG, "Error starting, microphone was stopped or not created, "
+ "use createMicrophone() before start()");
}
}
/**
* Call when you need mic samples.
* This method will block until numBytes worth of samples are ready.
*/
public int read(ByteBuffer directBuffer, int numBytes) {
directBuffer.rewind();
// write to the buffer and return number of bytes written.
return audioRecord.read(directBuffer, numBytes);
}
/**
* Stop and release microphone
*/
public synchronized void stop() {
// handlerThread must not be null, else the stop impl will throw
handlerThread = new HandlerThread("nothing");
super.stop();
}
}

View File

@ -0,0 +1,9 @@
package com.pedro.encoder.input.audio;
public class NoAudioEffect extends CustomAudioEffect {
@Override
public byte[] process(byte[] pcmBuffer) {
return pcmBuffer;
}
}

View File

@ -0,0 +1,243 @@
package com.pedro.encoder.input.decoder;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.util.Log;
import com.pedro.encoder.Frame;
import com.pedro.encoder.input.audio.GetMicrophoneData;
import com.pedro.encoder.utils.PCMUtil;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Created by pedro on 20/06/17.
*/
public class AudioDecoder {
private final String TAG = "AudioDecoder";
private AudioDecoderInterface audioDecoderInterface;
private LoopFileInterface loopFileInterface;
private MediaExtractor audioExtractor;
private MediaCodec audioDecoder;
private MediaCodec.BufferInfo audioInfo = new MediaCodec.BufferInfo();
private boolean decoding;
private Thread thread;
private GetMicrophoneData getMicrophoneData;
private MediaFormat audioFormat;
private String mime = "";
private int sampleRate;
private boolean isStereo;
private int channels = 1;
private int size = 2048;
private byte[] pcmBuffer = new byte[size];
private byte[] pcmBufferMuted = new byte[11];
private static boolean loopMode = false;
private boolean muted = false;
private long duration;
private volatile long seekTime = 0;
private volatile long startMs = 0;
public AudioDecoder(GetMicrophoneData getMicrophoneData,
AudioDecoderInterface audioDecoderInterface, LoopFileInterface loopFileInterface) {
this.getMicrophoneData = getMicrophoneData;
this.audioDecoderInterface = audioDecoderInterface;
this.loopFileInterface = loopFileInterface;
}
public boolean initExtractor(String filePath) throws IOException {
decoding = false;
audioExtractor = new MediaExtractor();
audioExtractor.setDataSource(filePath);
for (int i = 0; i < audioExtractor.getTrackCount() && !mime.startsWith("audio/"); i++) {
audioFormat = audioExtractor.getTrackFormat(i);
mime = audioFormat.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
audioExtractor.selectTrack(i);
} else {
audioFormat = null;
}
}
if (audioFormat != null) {
channels = audioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
isStereo = channels >= 2;
sampleRate = audioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
duration = audioFormat.getLong(MediaFormat.KEY_DURATION);
if (channels >= 2) {
pcmBuffer = new byte[2048 * channels];
}
return true;
//audio decoder not supported
} else {
mime = "";
audioFormat = null;
return false;
}
}
public boolean prepareAudio() {
try {
audioDecoder = MediaCodec.createDecoderByType(mime);
audioDecoder.configure(audioFormat, null, null, 0);
return true;
} catch (IOException e) {
Log.e(TAG, "Prepare decoder error:", e);
return false;
}
}
public void start() {
decoding = true;
audioDecoder.start();
thread = new Thread(new Runnable() {
@Override
public void run() {
try {
decodeAudio();
} catch (IllegalStateException e) {
Log.i(TAG, "Decoding error", e);
}
}
});
thread.start();
}
public void stop() {
decoding = false;
seekTime = 0;
if (thread != null) {
thread.interrupt();
try {
thread.join(100);
} catch (InterruptedException e) {
thread.interrupt();
}
thread = null;
}
try {
audioDecoder.stop();
audioDecoder.release();
audioDecoder = null;
} catch (IllegalStateException | NullPointerException e) {
audioDecoder = null;
}
if (audioExtractor != null) {
audioExtractor.release();
audioExtractor = null;
}
}
private void decodeAudio() throws IllegalStateException {
ByteBuffer[] inputBuffers = audioDecoder.getInputBuffers();
ByteBuffer[] outputBuffers = audioDecoder.getOutputBuffers();
startMs = System.currentTimeMillis();
while (decoding) {
int inIndex = audioDecoder.dequeueInputBuffer(10000);
if (inIndex >= 0) {
ByteBuffer buffer = inputBuffers[inIndex];
int sampleSize = audioExtractor.readSampleData(buffer, 0);
if (sampleSize < 0) {
audioDecoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
audioDecoder.queueInputBuffer(inIndex, 0, sampleSize, audioExtractor.getSampleTime(), 0);
audioExtractor.advance();
}
int outIndex = audioDecoder.dequeueOutputBuffer(audioInfo, 10000);
switch (outIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
outputBuffers = audioDecoder.getOutputBuffers();
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
case MediaCodec.INFO_TRY_AGAIN_LATER:
break;
default:
//needed for fix decode speed
while (audioExtractor.getSampleTime() / 1000
> System.currentTimeMillis() - startMs + seekTime) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
if (thread != null) thread.interrupt();
return;
}
}
ByteBuffer outBuffer = outputBuffers[outIndex];
//This buffer is PCM data
if (muted) {
outBuffer.get(pcmBufferMuted, 0,
outBuffer.remaining() <= pcmBufferMuted.length ? outBuffer.remaining()
: pcmBufferMuted.length);
getMicrophoneData.inputPCMData(new Frame(pcmBufferMuted, 0, pcmBufferMuted.length));
} else {
outBuffer.get(pcmBuffer, 0,
outBuffer.remaining() <= pcmBuffer.length ? outBuffer.remaining()
: pcmBuffer.length);
if (channels > 2) { //downgrade to stereo
byte[] bufferStereo = PCMUtil.pcmToStereo(pcmBuffer, channels);
getMicrophoneData.inputPCMData(new Frame(bufferStereo, 0, bufferStereo.length));
} else {
getMicrophoneData.inputPCMData(new Frame(pcmBuffer, 0, pcmBuffer.length));
}
}
audioDecoder.releaseOutputBuffer(outIndex, false);
break;
}
// All decoded frames have been rendered, we can stop playing now
if ((audioInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
seekTime = 0;
Log.i(TAG, "end of file out");
if (loopMode) {
loopFileInterface.onReset(false);
} else {
audioDecoderInterface.onAudioDecoderFinished();
}
}
}
}
}
public double getTime() {
if (decoding) {
return audioExtractor.getSampleTime() / 10E5;
} else {
return 0;
}
}
public void moveTo(double time) {
audioExtractor.seekTo((long) (time * 10E5), MediaExtractor.SEEK_TO_CLOSEST_SYNC);
seekTime = audioExtractor.getSampleTime() / 1000;
startMs = System.currentTimeMillis();
}
public void setLoopMode(boolean loopMode) {
this.loopMode = loopMode;
}
public void mute() {
muted = true;
}
public void unMute() {
muted = false;
}
public boolean isMuted() {
return muted;
}
public int getSampleRate() {
return sampleRate;
}
public boolean isStereo() {
return isStereo;
}
public double getDuration() {
return duration / 10E5;
}
}

View File

@ -0,0 +1,10 @@
package com.pedro.encoder.input.decoder;
/**
* Created by pedro on 6/07/17.
*/
public interface AudioDecoderInterface {
void onAudioDecoderFinished();
}

View File

@ -0,0 +1,10 @@
package com.pedro.encoder.input.decoder;
/**
* Created by pedro on 4/03/18.
*/
public interface LoopFileInterface {
void onReset(boolean isVideo);
}

View File

@ -0,0 +1,187 @@
package com.pedro.encoder.input.decoder;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Created by pedro on 20/06/17.
*/
public class VideoDecoder {
private final String TAG = "VideoDecoder";
private VideoDecoderInterface videoDecoderInterface;
private LoopFileInterface loopFileInterface;
private MediaExtractor videoExtractor;
private MediaCodec videoDecoder;
private MediaCodec.BufferInfo videoInfo = new MediaCodec.BufferInfo();
private boolean decoding;
private Thread thread;
private MediaFormat videoFormat;
private String mime = "";
private int width;
private int height;
private long duration;
private static boolean loopMode = false;
private volatile long seekTime = 0;
private volatile long startMs = 0;
public VideoDecoder(VideoDecoderInterface videoDecoderInterface,
LoopFileInterface loopFileInterface) {
this.videoDecoderInterface = videoDecoderInterface;
this.loopFileInterface = loopFileInterface;
}
public boolean initExtractor(String filePath) throws IOException {
decoding = false;
videoExtractor = new MediaExtractor();
videoExtractor.setDataSource(filePath);
for (int i = 0; i < videoExtractor.getTrackCount() && !mime.startsWith("video/"); i++) {
videoFormat = videoExtractor.getTrackFormat(i);
mime = videoFormat.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
videoExtractor.selectTrack(i);
} else {
videoFormat = null;
}
}
if (videoFormat != null) {
width = videoFormat.getInteger(MediaFormat.KEY_WIDTH);
height = videoFormat.getInteger(MediaFormat.KEY_HEIGHT);
duration = videoFormat.getLong(MediaFormat.KEY_DURATION);
return true;
//video decoder not supported
} else {
mime = "";
videoFormat = null;
return false;
}
}
public boolean prepareVideo(Surface surface) {
try {
videoDecoder = MediaCodec.createDecoderByType(mime);
videoDecoder.configure(videoFormat, surface, null, 0);
return true;
} catch (IOException e) {
Log.e(TAG, "Prepare decoder error:", e);
return false;
}
}
public void start() {
decoding = true;
videoDecoder.start();
thread = new Thread(new Runnable() {
@Override
public void run() {
try {
decodeVideo();
} catch (IllegalStateException e) {
Log.i(TAG, "Decoding error", e);
}
}
});
thread.start();
}
public void stop() {
decoding = false;
seekTime = 0;
if (thread != null) {
thread.interrupt();
try {
thread.join(100);
} catch (InterruptedException e) {
thread.interrupt();
}
thread = null;
}
try {
videoDecoder.stop();
videoDecoder.release();
videoDecoder = null;
} catch (IllegalStateException | NullPointerException e) {
videoDecoder = null;
}
if (videoExtractor != null) {
videoExtractor.release();
videoExtractor = null;
}
}
private void decodeVideo() throws IllegalStateException {
ByteBuffer[] inputBuffers = videoDecoder.getInputBuffers();
startMs = System.currentTimeMillis();
while (decoding) {
int inIndex = videoDecoder.dequeueInputBuffer(10000);
if (inIndex >= 0) {
ByteBuffer buffer = inputBuffers[inIndex];
int sampleSize = videoExtractor.readSampleData(buffer, 0);
if (sampleSize < 0) {
videoDecoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
videoDecoder.queueInputBuffer(inIndex, 0, sampleSize, videoExtractor.getSampleTime(), 0);
videoExtractor.advance();
}
}
int outIndex = videoDecoder.dequeueOutputBuffer(videoInfo, 10000);
if (outIndex >= 0) {
while (videoExtractor.getSampleTime() / 1000
> System.currentTimeMillis() - startMs + seekTime) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
if (thread != null) thread.interrupt();
return;
}
}
videoDecoder.releaseOutputBuffer(outIndex, videoInfo.size != 0);
}
if ((videoInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
seekTime = 0;
Log.i(TAG, "end of file out");
if (loopMode) {
loopFileInterface.onReset(true);
} else {
videoDecoderInterface.onVideoDecoderFinished();
}
}
}
}
public double getTime() {
if (decoding) {
return videoExtractor.getSampleTime() / 10E5;
} else {
return 0;
}
}
public void moveTo(double time) {
videoExtractor.seekTo((long) (time * 10E5), MediaExtractor.SEEK_TO_CLOSEST_SYNC);
seekTime = videoExtractor.getSampleTime() / 1000;
startMs = System.currentTimeMillis();
}
public void setLoopMode(boolean loopMode) {
this.loopMode = loopMode;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public double getDuration() {
return duration / 10E5;
}
}

Some files were not shown because too many files have changed in this diff Show More