mirror of
https://github.com/lizongying/my-tv.git
synced 2026-10-27 14:46:30 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52c7887951 | ||
|
|
d8fffbb402 | ||
|
|
cc2dc25e35 | ||
|
|
aef165504f |
+8
-8
@@ -15,17 +15,17 @@ if (IS_SO_BUILD)
|
||||
# and CMake builds them for you. When you build your app, Gradle
|
||||
# automatically packages shared libraries with your APK.
|
||||
add_library( # Specifies the name of the library.
|
||||
native-lib
|
||||
native
|
||||
|
||||
# Sets the library as a shared library.
|
||||
SHARED
|
||||
|
||||
# Provides a relative path to your source file(s).
|
||||
src/main/cpp/native-lib.c)
|
||||
src/main/cpp/native.c)
|
||||
|
||||
# 设置编译输出路径
|
||||
set_target_properties(
|
||||
native-lib
|
||||
native
|
||||
PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY
|
||||
${CMAKE_SOURCE_DIR}/src/main/cpp/${ANDROID_ABI}
|
||||
@@ -36,18 +36,18 @@ else ()
|
||||
SHARED
|
||||
src/main/cpp/nothing.c)
|
||||
|
||||
add_library(native-lib
|
||||
add_library(native
|
||||
SHARED
|
||||
IMPORTED)
|
||||
|
||||
set_target_properties( # Specifies the target library.
|
||||
native-lib
|
||||
native
|
||||
|
||||
# Specifies the parameter you want to define.
|
||||
PROPERTIES IMPORTED_LOCATION
|
||||
|
||||
# Provides the path to the library you want to import.
|
||||
${CMAKE_SOURCE_DIR}/src/main/cpp/${ANDROID_ABI}/libnative-lib.so)
|
||||
${CMAKE_SOURCE_DIR}/src/main/cpp/${ANDROID_ABI}/libnative.so)
|
||||
endif ()
|
||||
|
||||
add_library(libssl
|
||||
@@ -87,7 +87,7 @@ find_library( # Defines the name of the path variable that stores the
|
||||
if (IS_SO_BUILD)
|
||||
# Links your native library against one or more other native libraries.
|
||||
target_link_libraries( # Specifies the target library.
|
||||
native-lib
|
||||
native
|
||||
libssl
|
||||
libcrypto
|
||||
${log-lib})
|
||||
@@ -96,6 +96,6 @@ else ()
|
||||
nothing
|
||||
libssl
|
||||
libcrypto
|
||||
native-lib
|
||||
native
|
||||
${log-lib})
|
||||
endif ()
|
||||
+9
-1
@@ -90,13 +90,21 @@ static def VersionName() {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def media3_version = "1.1.1"
|
||||
|
||||
// For media playback using ExoPlayer
|
||||
implementation "androidx.media3:media3-exoplayer:$media3_version"
|
||||
|
||||
// For HLS playback support with ExoPlayer
|
||||
implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
|
||||
|
||||
implementation 'com.google.protobuf:protobuf-kotlin:3.25.1'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-protobuf:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"
|
||||
implementation 'androidx.core:core-ktx:1.11.0-beta02'
|
||||
implementation 'androidx.leanback:leanback:1.0.0'
|
||||
implementation 'androidx.leanback:leanback:1.2.0-alpha02'
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0-RC")
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.MyTV">
|
||||
<activity
|
||||
android:keepScreenOn="true"
|
||||
android:name=".MainActivity"
|
||||
android:banner="@drawable/tv"
|
||||
android:exported="true"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,327 @@
|
||||
#include "stdio.h"
|
||||
#include "jni.h"
|
||||
#include <openssl/rand.h>
|
||||
#include <string.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/md5.h>
|
||||
#include <android/log.h>
|
||||
#include <stdbool.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#define APP_SIGNATURE "f0005d8a185c47e72946053a0af5c0f5620605dd9e301ef0937e477f607ab0d7"
|
||||
#define APP_SIGNATURE2 "9e58fc98c3967ae80eb25541241895368a900fa488e015b08614575c18fab95e"
|
||||
|
||||
#define TAG "native"
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
||||
|
||||
static bool signatureVerified = false;
|
||||
|
||||
static const char key[33] = "48e5918a74ae21c972b90cce8af6c8be";
|
||||
static const char iv[33] = "9a7e7d23610266b1d9fbf98581384d92";
|
||||
static const char appendString[15] = "0f$IVHi9Qno?G";
|
||||
|
||||
static void toUpperCase(char *str) {
|
||||
while (*str) {
|
||||
*str = toupper((unsigned char) *str);
|
||||
str++;
|
||||
}
|
||||
}
|
||||
|
||||
static char *binaryToHex(const unsigned char *data, size_t dataSize) {
|
||||
char *hexString = (char *) malloc(dataSize * 2 + 1); // 每个字节需要两个十六进制字符,再加上字符串结束符
|
||||
if (hexString == NULL) {
|
||||
// 内存分配失败
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < dataSize; ++i) {
|
||||
sprintf(hexString + i * 2, "%02X", data[i]);
|
||||
}
|
||||
|
||||
hexString[dataSize * 2] = '\0'; // 添加字符串结束符
|
||||
|
||||
return hexString;
|
||||
}
|
||||
|
||||
static unsigned char *hexStringToBinary(const char *hexString1) {
|
||||
size_t inputLength = strlen(hexString1);
|
||||
if (inputLength % 2 != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t length = inputLength / 2;
|
||||
unsigned char *result1 = (unsigned char *) malloc(length);
|
||||
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
char byteString[3] = {hexString1[2 * i], hexString1[2 * i + 1], '\0'};
|
||||
result1[i] = (unsigned char) strtoul(byteString, NULL, 16);
|
||||
}
|
||||
|
||||
return result1;
|
||||
}
|
||||
|
||||
static char *aes(const char *plaintext) {
|
||||
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
|
||||
if (ctx == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// const char k[33] = "48e5918a74ae21c972b90cce8af6c8be";
|
||||
unsigned char *resultKey = hexStringToBinary(key);
|
||||
|
||||
// const char i[33] = "9a7e7d23610266b1d9fbf98581384d92";
|
||||
unsigned char *resultIv = hexStringToBinary(iv);
|
||||
|
||||
if (EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, resultKey, resultIv) != 1) {
|
||||
free(resultKey);
|
||||
free(resultIv);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_set_padding(ctx, 1);
|
||||
|
||||
size_t dataLength = strlen(plaintext);
|
||||
|
||||
size_t maxEncryptedLength = dataLength + EVP_CIPHER_block_size(EVP_aes_128_cbc());
|
||||
unsigned char *encryptedData = (unsigned char *) malloc(maxEncryptedLength);
|
||||
if (encryptedData == NULL) {
|
||||
free(resultKey);
|
||||
free(resultIv);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int len;
|
||||
if (EVP_EncryptUpdate(ctx, encryptedData, &len, (const unsigned char *) plaintext,
|
||||
(int) dataLength) !=
|
||||
1) {
|
||||
free(resultKey);
|
||||
free(resultIv);
|
||||
free(encryptedData);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t encryptedLength = len;
|
||||
|
||||
if (EVP_EncryptFinal_ex(ctx, encryptedData + len, &len) != 1) {
|
||||
free(resultKey);
|
||||
free(resultIv);
|
||||
free(encryptedData);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 更新加密数据长度
|
||||
encryptedLength += len;
|
||||
|
||||
// 创建一个 C 字符串以容纳加密后的数据
|
||||
char *encryptedString = binaryToHex(encryptedData, encryptedLength);
|
||||
if (encryptedString == NULL) {
|
||||
// 内存分配失败
|
||||
free(resultKey);
|
||||
free(resultIv);
|
||||
free(encryptedData);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 释放内存和资源
|
||||
free(resultKey);
|
||||
free(resultIv);
|
||||
free(encryptedData);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
|
||||
return encryptedString;
|
||||
}
|
||||
|
||||
jbyte *appendStringToEnd(jbyte *data, size_t *dataLength) {
|
||||
// 获取原始数据长度
|
||||
size_t originalLength = *dataLength;
|
||||
|
||||
size_t appendStringLength = strlen(appendString);
|
||||
|
||||
// 分配新的内存空间,包括原始数据和附加字符串
|
||||
data = (jbyte *) realloc(data,
|
||||
originalLength + appendStringLength + 1);
|
||||
|
||||
if (data == NULL) {
|
||||
// 内存分配失败的处理
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 将附加字符串复制到新的内存空间的末尾
|
||||
memcpy(data + originalLength, appendString, appendStringLength);
|
||||
|
||||
// 在新数据的末尾添加 null 终止字符
|
||||
data[originalLength + appendStringLength] = '\0';
|
||||
|
||||
// 更新数据长度
|
||||
*dataLength = originalLength + appendStringLength + 1;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray
|
||||
|
||||
JNICALL
|
||||
Java_com_lizongying_mytv_Encryptor_hash(JNIEnv *env, jobject thiz, jbyteArray data_) {
|
||||
if (signatureVerified == false) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
jbyte *data = (*env)->GetByteArrayElements(env, data_, NULL);
|
||||
|
||||
// 获取数据长度
|
||||
size_t dataLength = (*env)->GetArrayLength(env, data_);
|
||||
|
||||
// LOGI("appendStringLength %s", data);
|
||||
data = appendStringToEnd(data, &dataLength);
|
||||
|
||||
dataLength = strlen((const char *) data);
|
||||
// LOGI("dataLength %d", dataLength);
|
||||
|
||||
// 分配内存以存储 MD5 值(16 字节)
|
||||
unsigned char md5Digest[EVP_MAX_MD_SIZE];
|
||||
unsigned int md5DigestLength;
|
||||
|
||||
// 创建 MD5 上下文
|
||||
EVP_MD_CTX *md5Context = EVP_MD_CTX_new();
|
||||
if (md5Context == NULL) {
|
||||
// 处理错误
|
||||
(*env)->ReleaseByteArrayElements(env, data_, data, 0);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 初始化 MD5 上下文
|
||||
if (EVP_DigestInit_ex(md5Context, EVP_md5(), NULL) != 1) {
|
||||
// 处理错误
|
||||
EVP_MD_CTX_free(md5Context);
|
||||
(*env)->ReleaseByteArrayElements(env, data_, data, 0);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 更新 MD5 上下文
|
||||
if (EVP_DigestUpdate(md5Context, data, dataLength) != 1) {
|
||||
// 处理错误
|
||||
EVP_MD_CTX_free(md5Context);
|
||||
(*env)->ReleaseByteArrayElements(env, data_, data, 0);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 获取 MD5 值
|
||||
if (EVP_DigestFinal_ex(md5Context, md5Digest, &md5DigestLength) != 1) {
|
||||
// 处理错误
|
||||
EVP_MD_CTX_free(md5Context);
|
||||
(*env)->ReleaseByteArrayElements(env, data_, data, 0);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
jsize md5Length = (jsize) md5DigestLength;
|
||||
|
||||
// 创建 Java 字节数组以容纳 MD5 值
|
||||
jbyteArray md5Array = (*env)->NewByteArray(env, md5Length);
|
||||
if (md5Array == NULL) {
|
||||
// 处理错误
|
||||
EVP_MD_CTX_free(md5Context);
|
||||
(*env)->ReleaseByteArrayElements(env, data_, data, 0);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 将 MD5 值复制到 Java 字节数组
|
||||
(*env)->SetByteArrayRegion(env, md5Array, 0, md5Length, (jbyte *) md5Digest);
|
||||
|
||||
// 释放内存和资源
|
||||
EVP_MD_CTX_free(md5Context);
|
||||
(*env)->ReleaseByteArrayElements(env, data_, data, 0);
|
||||
return md5Array;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lizongying_mytv_Encryptor_init(JNIEnv *env, jobject thiz, jobject context) {
|
||||
// 获取 Context 类
|
||||
jclass contextClass = (*env)->GetObjectClass(env, context);
|
||||
|
||||
// 获取 getAppSignature 方法的 ID
|
||||
jmethodID methodId = (*env)->GetMethodID(env, contextClass, "getAppSignature",
|
||||
"()Ljava/lang/String;");
|
||||
|
||||
// 调用 Kotlin 函数
|
||||
jstring result = (jstring) (*env)->CallObjectMethod(env, context, methodId);
|
||||
|
||||
// 检查 result 是否为 NULL
|
||||
if (result == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 在 C 端处理字符串
|
||||
const char *utfChars = (*env)->GetStringUTFChars(env, result, NULL);
|
||||
|
||||
// 比较签名哈希
|
||||
signatureVerified = (strcmp(utfChars, APP_SIGNATURE) == 0);
|
||||
|
||||
if (signatureVerified == 0 ) {
|
||||
signatureVerified = (strcmp(utfChars, APP_SIGNATURE2) == 0);
|
||||
}
|
||||
|
||||
// 释放字符串
|
||||
(*env)->ReleaseStringUTFChars(env, result, utfChars);
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_lizongying_mytv_Encryptor_encrypt(JNIEnv *env, jobject thiz, jstring t, jstring e,
|
||||
jstring r, jstring n, jstring i) {
|
||||
if (signatureVerified == false) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *t_str = (*env)->GetStringUTFChars(env, t, NULL);
|
||||
const char *e_str = (*env)->GetStringUTFChars(env, e, NULL);
|
||||
const char *r_str = (*env)->GetStringUTFChars(env, r, NULL);
|
||||
const char *n_str = (*env)->GetStringUTFChars(env, n, NULL);
|
||||
const char *i_str = (*env)->GetStringUTFChars(env, i, NULL);
|
||||
|
||||
char El[256];
|
||||
snprintf(El, sizeof(El),
|
||||
"|%s|%s|%s|%s|%s|%s|https://www.yangshipin.c|mozilla/5.0 (macintosh; ||Mozilla|Netscape|MacIntel|",
|
||||
t_str, e_str, "mg3c3b04ba", r_str, n_str, i_str);
|
||||
// LOGI("EL %s", El);
|
||||
|
||||
unsigned int xl = 0;
|
||||
for (int Ll = 0; Ll < strlen(El); Ll++) {
|
||||
xl = (xl << 5) - xl + (unsigned char) El[Ll];
|
||||
xl &= xl;
|
||||
}
|
||||
|
||||
char plaintext[256];
|
||||
snprintf(plaintext, sizeof(plaintext), "|%d%s", xl, El);
|
||||
|
||||
// Encrypt the plaintext
|
||||
char *encryptedResult = aes(plaintext);
|
||||
toUpperCase(encryptedResult);
|
||||
|
||||
char result[512];
|
||||
snprintf(result, sizeof(result), "--01%s", encryptedResult);
|
||||
|
||||
// LOGI("result %s", result);
|
||||
|
||||
// Free the memory allocated in the encrypt function
|
||||
free(encryptedResult);
|
||||
|
||||
(*env)->ReleaseStringUTFChars(env, t, t_str);
|
||||
(*env)->ReleaseStringUTFChars(env, e, e_str);
|
||||
(*env)->ReleaseStringUTFChars(env, r, r_str);
|
||||
(*env)->ReleaseStringUTFChars(env, n, n_str);
|
||||
(*env)->ReleaseStringUTFChars(env, i, i_str);
|
||||
|
||||
// Create a Java string from the result and return it
|
||||
jstring resultString = (*env)->NewStringUTF(env, result);
|
||||
|
||||
return resultString;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ class Encryptor {
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("native-lib")
|
||||
System.loadLibrary("native")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package com.lizongying.mytv
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaPlayer
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.view.SurfaceHolder
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.leanback.media.PlaybackGlueHost
|
||||
import androidx.leanback.media.PlayerAdapter
|
||||
import androidx.leanback.media.SurfaceHolderGlueHost
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
open class ExoPlayerAdapter(private var mContext: Context?) : PlayerAdapter() {
|
||||
|
||||
val mPlayer = mContext?.let { ExoPlayer.Builder(it).build() }
|
||||
var mSurfaceHolderGlueHost: SurfaceHolderGlueHost? = null
|
||||
val mRunnable: Runnable = object : Runnable {
|
||||
override fun run() {
|
||||
callback.onCurrentPositionChanged(this@ExoPlayerAdapter)
|
||||
mHandler.postDelayed(this, getProgressUpdatingInterval().toLong())
|
||||
}
|
||||
};
|
||||
val mHandler = Handler()
|
||||
var mInitialized = false // true when the MediaPlayer is prepared/initialized
|
||||
|
||||
var mMediaSourceUri: Uri? = null
|
||||
var mHasDisplay = false
|
||||
var mBufferedProgress: Long = 0
|
||||
|
||||
var mBufferingStart = false
|
||||
|
||||
open fun notifyBufferingStartEnd() {
|
||||
callback.onBufferingStateChanged(
|
||||
this@ExoPlayerAdapter,
|
||||
mBufferingStart || !mInitialized
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAttachedToHost(host: PlaybackGlueHost?) {
|
||||
if (host is SurfaceHolderGlueHost) {
|
||||
mSurfaceHolderGlueHost = host
|
||||
mSurfaceHolderGlueHost!!.setSurfaceHolderCallback(VideoPlayerSurfaceHolderCallback(this))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will reset the [MediaPlayer] and the glue such that a new file can be played. You are
|
||||
* not required to call this method before playing the first file. However you have to call it
|
||||
* before playing a second one.
|
||||
*/
|
||||
open fun reset() {
|
||||
changeToUnitialized()
|
||||
// mPlayer.reset()
|
||||
}
|
||||
|
||||
open fun changeToUnitialized() {
|
||||
if (mInitialized) {
|
||||
mInitialized = false
|
||||
notifyBufferingStartEnd()
|
||||
if (mHasDisplay) {
|
||||
callback.onPreparedStateChanged(this@ExoPlayerAdapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release internal MediaPlayer. Should not use the object after call release().
|
||||
*/
|
||||
open fun release() {
|
||||
changeToUnitialized()
|
||||
mHasDisplay = false
|
||||
mPlayer?.release()
|
||||
}
|
||||
|
||||
override fun onDetachedFromHost() {
|
||||
if (mSurfaceHolderGlueHost != null) {
|
||||
mSurfaceHolderGlueHost!!.setSurfaceHolderCallback(null)
|
||||
mSurfaceHolderGlueHost = null
|
||||
}
|
||||
reset()
|
||||
release()
|
||||
}
|
||||
|
||||
/**
|
||||
* @see MediaPlayer.setDisplay
|
||||
*/
|
||||
fun setDisplay(surfaceHolder: SurfaceHolder?) {
|
||||
val hadDisplay = mHasDisplay
|
||||
mHasDisplay = surfaceHolder != null
|
||||
if (hadDisplay == mHasDisplay) {
|
||||
return
|
||||
}
|
||||
mPlayer?.setVideoSurfaceHolder(surfaceHolder)
|
||||
if (mHasDisplay) {
|
||||
if (mInitialized) {
|
||||
callback.onPreparedStateChanged(this@ExoPlayerAdapter)
|
||||
}
|
||||
} else {
|
||||
if (mInitialized) {
|
||||
callback.onPreparedStateChanged(this@ExoPlayerAdapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setProgressUpdatingEnabled(enabled: Boolean) {
|
||||
mHandler.removeCallbacks(mRunnable)
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
mHandler.postDelayed(mRunnable, getProgressUpdatingInterval().toLong())
|
||||
}
|
||||
|
||||
/**
|
||||
* Return updating interval of progress UI in milliseconds. Subclass may override.
|
||||
* @return Update interval of progress UI in milliseconds.
|
||||
*/
|
||||
open fun getProgressUpdatingInterval(): Int {
|
||||
return 16
|
||||
}
|
||||
|
||||
override fun isPlaying(): Boolean {
|
||||
return mInitialized && mPlayer?.isPlaying ?: false
|
||||
}
|
||||
|
||||
override fun getDuration(): Long {
|
||||
if (mInitialized) {
|
||||
val duration = mPlayer?.duration
|
||||
if (duration != null) {
|
||||
return duration.toLong()
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun getCurrentPosition(): Long {
|
||||
if (mInitialized) {
|
||||
val currentPosition = mPlayer?.currentPosition
|
||||
if (currentPosition != null) {
|
||||
return currentPosition.toLong()
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun play() {
|
||||
if (!mInitialized || mPlayer?.isPlaying == true) {
|
||||
return
|
||||
}
|
||||
mPlayer?.play()
|
||||
callback.onPlayStateChanged(this@ExoPlayerAdapter)
|
||||
callback.onCurrentPositionChanged(this@ExoPlayerAdapter)
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
if (isPlaying) {
|
||||
mPlayer?.pause()
|
||||
callback.onPlayStateChanged(this@ExoPlayerAdapter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun seekTo(newPosition: Long) {
|
||||
if (!mInitialized) {
|
||||
return
|
||||
}
|
||||
mPlayer?.seekTo(newPosition.toInt().toLong())
|
||||
}
|
||||
|
||||
override fun getBufferedPosition(): Long {
|
||||
return mBufferedProgress
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media source of the player witha given URI.
|
||||
*
|
||||
* @return Returns `true` if uri represents a new media; `false`
|
||||
* otherwise.
|
||||
* @see MediaPlayer.setDataSource
|
||||
*/
|
||||
open fun setDataSource(uri: Uri?): Boolean {
|
||||
if (if (mMediaSourceUri != null) mMediaSourceUri == uri else uri == null) {
|
||||
return false
|
||||
}
|
||||
mMediaSourceUri = uri
|
||||
prepareMediaForPlaying()
|
||||
return true
|
||||
}
|
||||
|
||||
private var mHeaders: Map<String, String>? = mapOf()
|
||||
|
||||
fun setHeaders(headers: Map<String, String>) {
|
||||
mHeaders = headers
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun prepareMediaForPlaying() {
|
||||
reset()
|
||||
try {
|
||||
if (mMediaSourceUri != null) {
|
||||
val httpDataSource = DefaultHttpDataSource.Factory()
|
||||
mHeaders?.let { httpDataSource.setDefaultRequestProperties(it) }
|
||||
|
||||
val hlsMediaSource =
|
||||
HlsMediaSource.Factory(httpDataSource).createMediaSource(
|
||||
MediaItem.fromUri(
|
||||
mMediaSourceUri!!
|
||||
)
|
||||
)
|
||||
mPlayer?.setMediaSource(hlsMediaSource)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
mPlayer?.prepare()
|
||||
mPlayer?.playWhenReady = true
|
||||
callback.onPlayStateChanged(this@ExoPlayerAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if MediaPlayer OnPreparedListener is invoked and got a SurfaceHolder if
|
||||
* [PlaybackGlueHost] provides SurfaceHolder.
|
||||
*/
|
||||
override fun isPrepared(): Boolean {
|
||||
return mInitialized && (mSurfaceHolderGlueHost == null || mHasDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
internal class VideoPlayerSurfaceHolderCallback(private val playerAdapter: ExoPlayerAdapter) :
|
||||
SurfaceHolder.Callback {
|
||||
|
||||
override fun surfaceCreated(surfaceHolder: SurfaceHolder) {
|
||||
playerAdapter.setDisplay(surfaceHolder)
|
||||
}
|
||||
|
||||
override fun surfaceChanged(surfaceHolder: SurfaceHolder, i: Int, i1: Int, i2: Int) {
|
||||
// Handle surface changes if needed
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) {
|
||||
playerAdapter.setDisplay(null)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
@@ -32,6 +33,8 @@ class MainActivity : FragmentActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.main_browse_fragment, playbackFragment)
|
||||
|
||||
@@ -36,6 +36,16 @@ class MainFragment : BrowseSupportFragment() {
|
||||
|
||||
private var sharedPref: SharedPreferences? = null
|
||||
|
||||
// override fun onCreateView(
|
||||
// inflater: LayoutInflater,
|
||||
// container: ViewGroup?,
|
||||
// savedInstanceState: Bundle?
|
||||
// ): View? {
|
||||
// super.onCreate()
|
||||
// // 使用自定义的布局文件
|
||||
// return inflater.inflate(R.layout.custom_browse_fragment, container, false)
|
||||
// }
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
@@ -47,12 +57,12 @@ class MainFragment : BrowseSupportFragment() {
|
||||
setupEventListeners()
|
||||
|
||||
view?.post {
|
||||
request?.fetchPage()
|
||||
// request?.fetchPage()
|
||||
}
|
||||
|
||||
tvListViewModel.getListLiveData().value?.forEach { tvViewModel ->
|
||||
tvViewModel.videoUrl.observe(viewLifecycleOwner) { _ ->
|
||||
Log.i(TAG, "tv ${tvViewModel.getTV()}")
|
||||
// Log.i(TAG, "tv ${tvViewModel.getTV()}")
|
||||
if (tvViewModel.updateByYSP()) {
|
||||
val tv = tvViewModel.getTV()
|
||||
if (tv.id == itemPosition) {
|
||||
@@ -66,7 +76,6 @@ class MainFragment : BrowseSupportFragment() {
|
||||
|
||||
private fun setupUIElements() {
|
||||
brandColor = ContextCompat.getColor(context!!, R.color.fastlane_background)
|
||||
// headersState = HEADERS_DISABLED
|
||||
}
|
||||
|
||||
private fun updateRows(tv: TV) {
|
||||
@@ -87,7 +96,6 @@ class MainFragment : BrowseSupportFragment() {
|
||||
|
||||
rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
|
||||
|
||||
// val cardPresenter = CardPresenter(lifecycleScope, viewLifecycleOwner)
|
||||
val cardPresenter = CardPresenter(lifecycleScope)
|
||||
|
||||
var idx: Long = 0
|
||||
@@ -112,17 +120,29 @@ class MainFragment : BrowseSupportFragment() {
|
||||
adapter = rowsAdapter
|
||||
|
||||
itemPosition = sharedPref?.getInt("position", 0)!!
|
||||
if (itemPosition >= tvListViewModel.size()) {
|
||||
itemPosition = 0
|
||||
savePosition(0)
|
||||
}
|
||||
|
||||
val tv = list2[itemPosition].item
|
||||
|
||||
val tvModel = tvListViewModel.getTVModel(itemPosition)
|
||||
if (tvModel?.ysp() != null) {
|
||||
Log.i(TAG, "ysp ${tvModel.getTV()}")
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
tvModel.let { request?.fetchData(it) }
|
||||
}
|
||||
} else {
|
||||
(activity as? MainActivity)?.play(list2[itemPosition].item)
|
||||
// (activity as? MainActivity)?.switchInfoFragment(list2[itemPosition].item)
|
||||
(activity as? MainActivity)?.play(tv)
|
||||
// (activity as? MainActivity)?.switchInfoFragment(tv)
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
activity,
|
||||
tv.title,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
(activity as? MainActivity)?.switchMainFragment()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import androidx.leanback.media.MediaPlayerAdapter
|
||||
import androidx.leanback.media.PlaybackTransportControlGlue
|
||||
import androidx.leanback.media.PlayerAdapter
|
||||
|
||||
class PlaybackControlGlue(
|
||||
context: Context?,
|
||||
playerAdapter: MediaPlayerAdapter?,
|
||||
playerAdapter: PlayerAdapter?,
|
||||
) :
|
||||
PlaybackTransportControlGlue<MediaPlayerAdapter>(context, playerAdapter) {
|
||||
PlaybackTransportControlGlue<PlayerAdapter>(context, playerAdapter) {
|
||||
|
||||
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (event!!.action == KeyEvent.ACTION_DOWN) {
|
||||
|
||||
@@ -5,21 +5,21 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.leanback.app.VideoSupportFragment
|
||||
import androidx.leanback.app.VideoSupportFragmentGlueHost
|
||||
import androidx.leanback.media.MediaPlayerAdapter
|
||||
import androidx.leanback.media.PlayerAdapter
|
||||
import androidx.leanback.media.PlaybackTransportControlGlue
|
||||
import androidx.leanback.widget.PlaybackControlsRow
|
||||
import java.io.IOException
|
||||
|
||||
class PlaybackFragment : VideoSupportFragment() {
|
||||
|
||||
private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<MediaPlayerAdapter>
|
||||
private var playerAdapter: MediaPlayerAdapter? = null
|
||||
private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<PlayerAdapter>
|
||||
private var playerAdapter: ExoPlayerAdapter? = null
|
||||
private var lastVideoUrl: String = ""
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
playerAdapter = MediaPlayerAdapter(context)
|
||||
playerAdapter = ExoPlayerAdapter(context)
|
||||
playerAdapter?.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_NONE)
|
||||
|
||||
view?.isFocusable = false
|
||||
@@ -60,15 +60,6 @@ class PlaybackFragment : VideoSupportFragment() {
|
||||
hideControlsOverlay(false)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (playerAdapter?.mediaPlayer != null) {
|
||||
playerAdapter?.release()
|
||||
Log.d(TAG, "playerAdapter released")
|
||||
}
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PlaybackVideoFragment"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.lizongying.mytv
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.lizongying.mytv.api.ApiClient
|
||||
import com.lizongying.mytv.api.LiveInfo
|
||||
@@ -14,6 +15,9 @@ import com.lizongying.mytv.proto.Ysp.cn.yangshipin.oms.common.proto.pageModel
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
||||
class Request(var context: Context) {
|
||||
@@ -85,13 +89,36 @@ class Request(var context: Context) {
|
||||
override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) {
|
||||
if (response.isSuccessful) {
|
||||
val liveInfo = response.body()
|
||||
Log.i(TAG, "liveInfo $liveInfo")
|
||||
if (liveInfo?.data?.playurl != null) {
|
||||
tvModel.updateVideoUrlByYSP(liveInfo.data.playurl)
|
||||
val chanll = liveInfo.data.chanll
|
||||
val decodedBytes = Base64.decode(
|
||||
chanll.substring(9, chanll.length - 3),
|
||||
Base64.DEFAULT
|
||||
)
|
||||
val decodedString = String(decodedBytes)
|
||||
val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""")
|
||||
val matchResult = regex.find(decodedString)
|
||||
var keyBytes = byteArrayOf()
|
||||
var ivBytes = byteArrayOf()
|
||||
|
||||
if (matchResult != null) {
|
||||
val (key, iv) = matchResult.destructured
|
||||
keyBytes = Base64.decode(key, Base64.DEFAULT)
|
||||
ivBytes = Base64.decode(iv, Base64.DEFAULT)
|
||||
}
|
||||
tvModel.updateVideoUrlByYSP(
|
||||
liveInfo.data.playurl + "&revoi=" + encryptTripleDES(
|
||||
keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0),
|
||||
ivBytes
|
||||
).uppercase()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<LiveInfo>, t: Throwable) {
|
||||
Log.i(TAG, "get data error")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -152,6 +179,22 @@ class Request(var context: Context) {
|
||||
})
|
||||
}
|
||||
|
||||
private fun encryptTripleDES(key: ByteArray, iv: ByteArray): String {
|
||||
var plaintext =
|
||||
"""{"mver":"1","subver":"1.2","host":"www.yangshipin.cn/#/tv/home?pid=","referer":"","canvas":"YSPANGLE(Apple,AppleM1Pro,OpenGL4.1)"}"""
|
||||
return try {
|
||||
val keySpec = SecretKeySpec(key, "DESede")
|
||||
val ivSpec = IvParameterSpec(iv)
|
||||
val cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
|
||||
val encryptedBytes = cipher.doFinal(plaintext.toByteArray())
|
||||
return encryptedBytes.let { it -> it.joinToString("") { "%02x".format(it) } }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "Request"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ data class LiveInfo(
|
||||
)
|
||||
|
||||
data class LiveInfoData(
|
||||
val playurl: String
|
||||
val chanll: String,
|
||||
val playurl: String,
|
||||
)
|
||||
|
||||
data class LiveInfoRequest(
|
||||
|
||||
@@ -68,7 +68,7 @@ class YSP(var context: Context) {
|
||||
cnlid = tvModel.getTV().sid
|
||||
defn = "fhd"
|
||||
|
||||
guid = getGuid()
|
||||
// guid = getGuid()
|
||||
randStr = getRand()
|
||||
timeStr = getTimeStr()
|
||||
|
||||
@@ -80,7 +80,6 @@ class YSP(var context: Context) {
|
||||
encryptor!!.encrypt(cnlid, timeStr, appVer, guid, platform)
|
||||
signature = getSignature()
|
||||
return """{"cnlid":"$cnlid","livepid":"$livepid","stream":"$stream","guid":"$guid","cKey":"$cKey","adjust":$adjust,"sphttps":"$sphttps","platform":"$platform","cmd":"$cmd","encryptVer":"$encryptVer","dtype":"$dtype","devid":"$devid","otype":"$otype","appVer":"$appVer","app_version":"$appVersion","rand_str":"$randStr","channel":"$channel","defn":"$defn","signature":"$signature"}"""
|
||||
|
||||
}
|
||||
|
||||
private fun getTimeStr(): String {
|
||||
@@ -100,7 +99,7 @@ class YSP(var context: Context) {
|
||||
}
|
||||
|
||||
private fun getGuid(): String {
|
||||
// var guid = sharedPref?.getString("guid", "").toString()
|
||||
var guid = sharedPref?.getString("guid", "").toString()
|
||||
if (guid == "") {
|
||||
guid = generateGuid()
|
||||
with(sharedPref!!.edit()) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.lizongying.mytv.models
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.lizongying.mytv.TV
|
||||
@@ -41,4 +40,8 @@ class TVListViewModel : ViewModel() {
|
||||
fun getTVModel(id: Int): TVViewModel? {
|
||||
return tvModelListLiveData.value?.get(id)
|
||||
}
|
||||
|
||||
fun size(): Int {
|
||||
return tvListLiveData.value!!.size
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- res/layout/custom_browse_fragment.xml -->
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/right_content_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_toRightOf="@+id/browse_headers" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/browse_headers"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true" />
|
||||
</RelativeLayout>
|
||||
Reference in New Issue
Block a user