Skip to content

Commit

Permalink
Releases/v1.3.0 (#13)
Browse files Browse the repository at this point in the history
* feat: 新增Quark网盘直连

* fix: 修复某些低端设备上播放视频会黑屏的问题

* perf: 优化投屏页和简介页流体效果性能

* Update README.md

---------

Co-authored-by: GhostenEditor <>
  • Loading branch information
GhostenEditor authored Dec 19, 2024
1 parent 6faed75 commit b5aaf09
Show file tree
Hide file tree
Showing 48 changed files with 1,040 additions and 487 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

[下载](https://github.com/GhostenEditor/Ghosten-Player/releases/latest)

一款同时适配Android TV和Android Phone的视频播放器,同时支持云播放(阿里云盘和Webdav)和本地播放,支持刮削影视的元信息,界面简洁纯净,操作简单
一款同时适配Android TV和Android Phone的视频播放器,同时支持云播放(阿里云盘、夸克网盘和Webdav)和本地播放,支持刮削影视的元信息,界面简洁纯净,操作简单

[^1]: 开发中

Expand Down Expand Up @@ -63,7 +63,7 @@
## Features

1. 支持 **Android TV****Android Phone** (桌面端开发中)
2. [支持阿里云盘、Webdav和本地文件播放](#添加账号)
2. [支持阿里云盘、夸克网盘、Webdav和本地文件播放](#添加账号)
3. 纯本地运行,无需后端服务支持 [^3]
4. [支持跳过片头/片尾](#跳过片头片尾)
5. 支持视频轨道选择
Expand Down Expand Up @@ -135,13 +135,19 @@ _**[Media3文档](https://developer.android.google.cn/media/media3/exoplayer/sup
| 3 | 客户端ID | 仅开发者账号提供 |
| 4 | 客户端密码 | 仅开发者账号提供 |

<img alt="Alipan Login Page" src="https://github.com/user-attachments/assets/49f6a2d0-c1b4-4fd1-8012-027442fe73ce" width="315"/>
<img alt="Alipan Login Page" src="https://github.com/user-attachments/assets/224c7dbf-a3cc-42d0-afc2-8122ff939c5d" width="315"/>

#### 夸克网盘

通过网页登录夸克后,点击右上角确认按钮后完成登录

<img alt="Quark Login Page" src="https://github.com/user-attachments/assets/7a5671b5-82f6-444a-ae4c-d16f85ce7a5a" width="315"/>

#### Webdav

填写Webdav对应的IP端口,输出账号密码,提交后完成登录。注:目前仅支持Basic编码登录

<img alt="Webdav Login Page 1" src="https://github.com/user-attachments/assets/68dd1b9f-9627-4cf4-8c65-d919795f42b8" width="315"/>
<img alt="Webdav Login Page 1" src="https://github.com/user-attachments/assets/29c72a9e-b61f-41e0-8d77-9f584142e64c" width="315"/>

### 添加资源

Expand Down
8 changes: 8 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.software.leanback"
android:required="true"/>
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false"/>

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
Expand Down Expand Up @@ -31,6 +38,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
Expand Down
258 changes: 158 additions & 100 deletions android/app/src/main/kotlin/com/ghosten/player/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,138 +1,196 @@
package com.ghosten.player

import android.annotation.TargetApi
import android.app.UiModeManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity(), MethodChannel.MethodCallHandler {
private var externalUrl: String? = null
private var deeplink: String? = null
private var channel: MethodChannel? = null
private var pipChannel: EventChannel? = null
private var pipSink: EventChannel.EventSink? = null
private var screenChannel: EventChannel? = null
private var screenSink: EventChannel.EventSink? = null
private var deeplinkChannel: EventChannel? = null
private var deeplinkSink: EventChannel.EventSink? = null
private val screenStateReceiver = ScreenStateReceiver()

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PLUGIN_NAMESPACE)
channel!!.setMethodCallHandler(this)
pipChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "$PLUGIN_NAMESPACE/pip")
pipChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
pipSink = sink
}
import android.os.Build
import android.os.Bundle
import android.window.BackEvent
import android.window.OnBackAnimationCallback
import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
import androidx.annotation.RequiresApi
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import io.flutter.Build.API_LEVELS
import io.flutter.embedding.android.FlutterFragment

override fun onCancel(args: Any?) {
pipSink?.endOfStream()
pipSink = null
}
})
screenChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "$PLUGIN_NAMESPACE/screen")
screenChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
screenSink = sink
screenSink?.success(SCREEN_MODE_PRESENT)
}
class MainActivity : FragmentActivity() {
private var mainFragment: MainFragment? = null
private var hasRegisteredBackCallback = false
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.NormalTheme)
setContentView(R.layout.main_layout)

override fun onCancel(args: Any?) {
screenSink?.endOfStream()
screenSink = null
}
})
deeplinkChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "$PLUGIN_NAMESPACE/deeplink")
deeplinkChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
deeplinkSink = sink
deeplinkSink?.success(deeplink)
}
super.onCreate(savedInstanceState)

if (intent.scheme == "content") {
mainFragment = ensureFlutterFragmentCreated(
PLAYER_FRAGMENT, "player", listOf(androidDeviceType().toString(), intent.data?.toString())
)
} else {
mainFragment = ensureFlutterFragmentCreated(MAIN_FRAGMENT, "main", listOf(androidDeviceType().toString()))
}
registerOnBackInvokedCallback()
}

private fun registerOnBackInvokedCallback() {
if (!hasRegisteredBackCallback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT, createOnBackInvokedCallback()
)
hasRegisteredBackCallback = true
}
}

override fun onCancel(args: Any?) {
deeplinkSink?.endOfStream()
deeplinkSink = null
private fun createOnBackInvokedCallback(): OnBackInvokedCallback {
if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) {
return object : OnBackAnimationCallback {
override fun onBackInvoked() {
commitBackGesture()
}

override fun onBackCancelled() {
cancelBackGesture()
}

override fun onBackProgressed(backEvent: BackEvent) {
updateBackGestureProgress(backEvent)
}

override fun onBackStarted(backEvent: BackEvent) {
startBackGesture(backEvent)
}
}
})
}

return OnBackInvokedCallback { mainFragment?.onBackPressed() }
}

override fun onResume() {
super.onResume()
if (intent.scheme == "content") {
externalUrl = intent.data?.toString()
@TargetApi(API_LEVELS.API_34)
@RequiresApi(API_LEVELS.API_34)
fun startBackGesture(backEvent: BackEvent) {
if (stillAttachedForEvent("startBackGesture")) {
mainFragment?.startBackGesture(backEvent)
}
val mScreenStatusFilter = IntentFilter()
mScreenStatusFilter.addAction(Intent.ACTION_SCREEN_ON)
mScreenStatusFilter.addAction(Intent.ACTION_SCREEN_OFF)
mScreenStatusFilter.addAction(Intent.ACTION_USER_PRESENT)
context.registerReceiver(screenStateReceiver, mScreenStatusFilter)
}

override fun onNewIntent(intent: Intent) {
if (intent.scheme == "ghosten") {
deeplink = intent.data?.toString()
deeplinkSink?.success(deeplink)
@TargetApi(API_LEVELS.API_34)
@RequiresApi(API_LEVELS.API_34)
fun updateBackGestureProgress(backEvent: BackEvent) {
if (stillAttachedForEvent("updateBackGestureProgress")) {
mainFragment?.updateBackGestureProgress(backEvent)
}
}

@TargetApi(API_LEVELS.API_34)
@RequiresApi(API_LEVELS.API_34)
fun commitBackGesture() {
if (stillAttachedForEvent("commitBackGesture")) {
mainFragment?.commitBackGesture()
}
}

@TargetApi(API_LEVELS.API_34)
@RequiresApi(API_LEVELS.API_34)
fun cancelBackGesture() {
if (stillAttachedForEvent("cancelBackGesture")) {
mainFragment?.cancelBackGesture()
}
}

private fun stillAttachedForEvent(event: String): Boolean {
if (mainFragment == null) {
return false
}
return true
}

@RequiresApi(Build.VERSION_CODES.O)
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
mainFragment?.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
}

override fun onPostResume() {
super.onPostResume()
mainFragment?.onPostResume()
}

override fun onNewIntent(intent: Intent) {
mainFragment?.onNewIntent(intent)
super.onNewIntent(intent)
}

override fun onPause() {
super.onPause()
context.unregisterReceiver(screenStateReceiver)
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
mainFragment?.onBackPressed()
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
mainFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults)
}

@Deprecated("Deprecated in Java")
override fun onActivityResult(
requestCode: Int, resultCode: Int, data: Intent?
) {
super.onActivityResult(requestCode, resultCode, data)
mainFragment?.onActivityResult(requestCode, resultCode, data)
}

override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
pipSink?.success(isInPictureInPictureMode)
override fun onUserLeaveHint() {
mainFragment?.onUserLeaveHint()
}

override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"androidDeviceType" -> result.success(androidDeviceType())
"externalUrl" -> result.success(externalUrl)
else -> result.notImplemented()
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
mainFragment?.onTrimMemory(level)
}


private fun ensureFlutterFragmentCreated(tag: String, entryPoint: String, args: List<String?>): MainFragment {
val fragmentManager: FragmentManager = supportFragmentManager
var fragment = fragmentManager.findFragmentByTag(tag) as MainFragment?

val newFragment = FlutterFragment
.NewEngineFragmentBuilder(MainFragment::class.java)
.shouldDelayFirstAndroidViewDraw(true)
.shouldAutomaticallyHandleOnBackPressed(true)
.dartEntrypoint(entryPoint)
.dartEntrypointArgs(args)
.build<MainFragment>()
if (fragment == null) {
fragmentManager.beginTransaction().add(R.id.fragment_container, newFragment, tag).commit()
} else {
fragmentManager.beginTransaction().replace(R.id.fragment_container, newFragment, tag).commit()
}
fragment = newFragment
return fragment;
}

private fun androidDeviceType(): Int {
val uiModeManager = getSystemService(UI_MODE_SERVICE) as UiModeManager
return if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
DEVICE_TYPE_TV
} else if (context.resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE) {
} else if (resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE) {
DEVICE_TYPE_PAD
} else {
DEVICE_TYPE_PHONE
}
}

inner class ScreenStateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val action = intent?.action
if (Intent.ACTION_SCREEN_ON == action) {
screenSink?.success(SCREEN_MODE_ON)
} else if (Intent.ACTION_SCREEN_OFF == action) {
screenSink?.success(SCREEN_MODE_OFF)
} else if (Intent.ACTION_USER_PRESENT == action) {
screenSink?.success(SCREEN_MODE_PRESENT)
}
}
}

companion object {
const val PLUGIN_NAMESPACE = "com.ghosten.player"
const val SCREEN_MODE_ON = "on"
const val SCREEN_MODE_OFF = "off"
const val SCREEN_MODE_PRESENT = "present"
const val DEVICE_TYPE_TV = 0
const val DEVICE_TYPE_PAD = 1
const val DEVICE_TYPE_PHONE = 2
private const val DEVICE_TYPE_TV = 0
private const val DEVICE_TYPE_PAD = 1
private const val DEVICE_TYPE_PHONE = 2
private const val MAIN_FRAGMENT = "main_fragment"
private const val PLAYER_FRAGMENT = "player_fragment"
}
}
Loading

0 comments on commit b5aaf09

Please sign in to comment.