From 1be8f14f071821efe9239105eddce175d73f9966 Mon Sep 17 00:00:00 2001 From: xiao12feng8 <16507319+xiao12feng8@user.noreply.gitee.com> Date: Mon, 5 Jan 2026 10:13:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=EF=BC=9A=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E6=97=A0=E6=B3=95=E6=AD=A3=E5=B8=B8=E5=BC=80=E6=92=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + .../livestreaming/BroadcastActivity.java | 133 ++-- .../livestreaming/RoomDetailActivity.java | 34 +- .../example/livestreaming/net/ApiService.java | 98 +++ .../main/res/drawable/bg_chat_input_dark.xml | 9 + .../main/res/layout/activity_broadcast.xml | 10 +- .../res/layout/activity_room_detail_new.xml | 570 ++++++++++-------- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 8 files changed, 529 insertions(+), 331 deletions(-) create mode 100644 android-app/app/src/main/res/drawable/bg_chat_input_dark.xml diff --git a/.gitignore b/.gitignore index fa98f379..ef5274f7 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,7 @@ archive/ Thumbs.db **/.idea/ **/.vscode/ +android-app/gradle/wrapper/gradle-wrapper.properties +android-app/gradle/wrapper/gradle-wrapper.properties +android-app/gradle/wrapper/gradle-wrapper.properties +android-app/gradle/wrapper/gradle-wrapper.properties diff --git a/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java b/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java index 53c7b0e3..0f743b50 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java @@ -9,7 +9,6 @@ import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.util.Log; -import android.util.Size; import android.view.SurfaceHolder; import android.view.View; import android.view.WindowManager; @@ -28,9 +27,11 @@ import com.example.livestreaming.net.AuthStore; import com.example.livestreaming.net.CreateRoomRequest; import com.example.livestreaming.net.Room; import com.pedro.encoder.input.video.CameraHelper; +import com.pedro.encoder.input.video.CameraOpenException; import com.pedro.rtmp.utils.ConnectCheckerRtmp; import com.pedro.rtplibrary.rtmp.RtmpCamera1; import com.pedro.rtplibrary.rtmp.RtmpCamera2; +import com.pedro.rtplibrary.view.OpenGlView; import java.util.Locale; import java.util.Map; @@ -42,7 +43,7 @@ import retrofit2.Response; /** * 手机开播界面 * 使用 RootEncoder 进行 RTMP 推流 - * 优先使用 Camera2 API (RtmpCamera2),兼容性更好 + * 使用 OpenGlView 确保视频编码正常工作 */ public class BroadcastActivity extends AppCompatActivity implements ConnectCheckerRtmp, SurfaceHolder.Callback { @@ -68,11 +69,11 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck private boolean streamerVerified = false; private boolean cameraInitialized = false; - // 推流参数 - 平衡画质和流畅度 - private static final int VIDEO_WIDTH = 720; - private static final int VIDEO_HEIGHT = 480; - private static final int VIDEO_FPS = 24; - private static final int VIDEO_BITRATE = 1200 * 1024; // 1.2Mbps + // 推流参数 - 使用标准16:9分辨率,兼容性更好 + private static final int VIDEO_WIDTH = 640; + private static final int VIDEO_HEIGHT = 360; // 标准16:9比例 + private static final int VIDEO_FPS = 25; // 标准帧率 + private static final int VIDEO_BITRATE = 800 * 1024; // 800kbps,更流畅 private static final int AUDIO_BITRATE = 64 * 1024; private static final int AUDIO_SAMPLE_RATE = 44100; @@ -239,7 +240,8 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck } private void setupSurface() { - binding.surfaceView.getHolder().addCallback(this); + // OpenGlView 使用 SurfaceHolder.Callback + binding.openGlView.getHolder().addCallback(this); } private void checkPermissions() { @@ -285,7 +287,7 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck } if (cameraInitialized) { - Log.d(TAG, "摄像头已初始化"); + Log.d(TAG, "摄像头已初始化,跳过"); return; } @@ -296,6 +298,8 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck return; } + // 立即设置标志,防止重复初始化 + cameraInitialized = true; Log.d(TAG, "开始初始化摄像头..."); // 延迟初始化,确保 Surface 完全准备好 @@ -304,6 +308,7 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck initCameraInternal(); } catch (Exception e) { Log.e(TAG, "摄像头初始化异常: " + e.getMessage(), e); + cameraInitialized = false; // 失败时重置标志 Toast.makeText(this, "摄像头初始化失败", Toast.LENGTH_LONG).show(); } }, 500); @@ -313,16 +318,40 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck // 优先尝试 Camera2 API (Android 5.0+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { try { - Log.d(TAG, "尝试使用 Camera2 API..."); - rtmpCamera2 = new RtmpCamera2(binding.surfaceView, this); + Log.d(TAG, "尝试使用 Camera2 API + OpenGlView..."); + // 【关键】使用 OpenGlView 构造 RtmpCamera2,确保视频编码正常 + rtmpCamera2 = new RtmpCamera2(binding.openGlView, this); - // 先开始预览,再准备编码器(推流时再准备) - String cameraId = isFrontCamera ? "1" : "0"; - rtmpCamera2.startPreview(cameraId); - useCamera2 = true; - cameraInitialized = true; - Log.d(TAG, "Camera2 预览已开始"); - return; + // 【关键】正确顺序:先准备编码器,再开始预览 + // 准备音频编码器 + boolean audioReady = rtmpCamera2.prepareAudio(AUDIO_BITRATE, AUDIO_SAMPLE_RATE, false); + Log.d(TAG, "Camera2 音频编码器准备: " + audioReady); + + // 准备视频编码器 - 尝试多种分辨率 + boolean videoReady = rtmpCamera2.prepareVideo(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS, VIDEO_BITRATE, 2); + Log.d(TAG, "Camera2 视频编码器准备 (640x360): " + videoReady); + + if (!videoReady) { + videoReady = rtmpCamera2.prepareVideo(480, 270, 20, 500 * 1024, 2); + Log.d(TAG, "Camera2 视频编码器准备 (480x270): " + videoReady); + } + + if (!videoReady) { + videoReady = rtmpCamera2.prepareVideo(320, 240, 15, 300 * 1024, 2); + Log.d(TAG, "Camera2 视频编码器准备 (320x240): " + videoReady); + } + + if (!audioReady || !videoReady) { + Log.e(TAG, "Camera2 编码器准备失败,尝试 Camera1"); + rtmpCamera2 = null; + } else { + // 编码器准备好后,开始预览 + String cameraId = isFrontCamera ? "1" : "0"; + rtmpCamera2.startPreview(cameraId); + useCamera2 = true; + Log.d(TAG, "Camera2 + OpenGlView 初始化完成,预览已开始"); + return; + } } catch (Exception e) { Log.w(TAG, "Camera2 初始化失败: " + e.getMessage(), e); if (rtmpCamera2 != null) { @@ -334,14 +363,33 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck // 回退到 Camera1 API try { - Log.d(TAG, "尝试使用 Camera1 API..."); - rtmpCamera1 = new RtmpCamera1(binding.surfaceView, this); + Log.d(TAG, "尝试使用 Camera1 API + OpenGlView..."); + // 【关键】使用 OpenGlView 构造 RtmpCamera1 + rtmpCamera1 = new RtmpCamera1(binding.openGlView, this); + + // 【关键】正确顺序:先准备编码器,再开始预览 + boolean audioReady = rtmpCamera1.prepareAudio(AUDIO_BITRATE, AUDIO_SAMPLE_RATE, false, false, false); + Log.d(TAG, "Camera1 音频编码器准备: " + audioReady); + + boolean videoReady = rtmpCamera1.prepareVideo(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS, VIDEO_BITRATE, 2); + Log.d(TAG, "Camera1 视频编码器准备 (640x360): " + videoReady); + + if (!videoReady) { + videoReady = rtmpCamera1.prepareVideo(480, 270, 20, 500 * 1024, 2); + Log.d(TAG, "Camera1 视频编码器准备 (480x270): " + videoReady); + } + + if (!audioReady || !videoReady) { + Log.e(TAG, "Camera1 编码器也准备失败"); + cameraInitialized = false; // 重置标志 + Toast.makeText(this, "编码器初始化失败", Toast.LENGTH_LONG).show(); + return; + } CameraHelper.Facing facing = isFrontCamera ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK; rtmpCamera1.startPreview(facing); useCamera2 = false; - cameraInitialized = true; - Log.d(TAG, "Camera1 预览已开始"); + Log.d(TAG, "Camera1 + OpenGlView 初始化完成,预览已开始"); return; } catch (Exception e) { Log.e(TAG, "Camera1 初始化也失败: " + e.getMessage(), e); @@ -351,6 +399,7 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck rtmpCamera1 = null; } + cameraInitialized = false; // 重置标志 Toast.makeText(this, "摄像头初始化失败,请检查权限或重启应用", Toast.LENGTH_LONG).show(); } @@ -484,53 +533,23 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck Log.d(TAG, "开始推流到: " + rtmpUrl); try { + // 编码器已在 initCameraInternal 中准备好,直接开始推流 if (useCamera2 && rtmpCamera2 != null) { Log.d(TAG, "使用 Camera2 API 推流"); - // 推流前准备编码器 - 使用优化后的低码率参数 - boolean audioReady = rtmpCamera2.prepareAudio(AUDIO_BITRATE, AUDIO_SAMPLE_RATE, false); - // 使用 640x360 低分辨率,更流畅 - boolean videoReady = rtmpCamera2.prepareVideo(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS, VIDEO_BITRATE, 0); - - Log.d(TAG, "编码器准备: audio=" + audioReady + ", video=" + videoReady); - - if (!videoReady) { - // 尝试更低的分辨率 - Log.w(TAG, "640x360 失败,尝试 480x270"); - videoReady = rtmpCamera2.prepareVideo(480, 270, VIDEO_FPS, VIDEO_BITRATE, 0); - } - - if (!videoReady) { - Log.e(TAG, "视频编码器准备失败"); - binding.progressBar.setVisibility(View.GONE); - binding.btnStartLive.setEnabled(true); - Toast.makeText(this, "视频编码器初始化失败", Toast.LENGTH_SHORT).show(); - return; - } - if (!rtmpCamera2.isStreaming()) { rtmpCamera2.startStream(rtmpUrl); + Log.d(TAG, "Camera2 推流已启动"); } + } else if (rtmpCamera1 != null) { Log.d(TAG, "使用 Camera1 API 推流"); - // 推流前准备编码器 - 使用优化参数 - boolean audioReady = rtmpCamera1.prepareAudio(AUDIO_BITRATE, AUDIO_SAMPLE_RATE, false, false, false); - boolean videoReady = rtmpCamera1.prepareVideo(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS, VIDEO_BITRATE, 0); - - Log.d(TAG, "编码器准备: audio=" + audioReady + ", video=" + videoReady); - - if (!videoReady) { - Log.e(TAG, "视频编码器准备失败"); - binding.progressBar.setVisibility(View.GONE); - binding.btnStartLive.setEnabled(true); - Toast.makeText(this, "视频编码器初始化失败", Toast.LENGTH_SHORT).show(); - return; - } - if (!rtmpCamera1.isStreaming()) { rtmpCamera1.startStream(rtmpUrl); + Log.d(TAG, "Camera1 推流已启动"); } + } else { Log.e(TAG, "没有可用的摄像头推流器"); binding.progressBar.setVisibility(View.GONE); diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java index 3d819970..93a1ebf7 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java @@ -3,6 +3,7 @@ package com.example.livestreaming; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -185,6 +186,9 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold getSupportActionBar().hide(); } + // 设置沉浸式全屏模式 + setupImmersiveMode(); + android.util.Log.d("RoomDetail", "开始inflate布局"); binding = ActivityRoomDetailNewBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); @@ -242,6 +246,28 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold } } + /** + * 设置沉浸式全屏模式 + */ + private void setupImmersiveMode() { + // 设置全屏沉浸式体验 + getWindow().setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + ); + + // 设置状态栏透明 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(android.graphics.Color.TRANSPARENT); + } + + // 隐藏导航栏(可选) + View decorView = getWindow().getDecorView(); + int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + decorView.setSystemUiVisibility(uiOptions); + } + private void setupUI() { // 返回按钮 binding.backButton.setOnClickListener(v -> finish()); @@ -954,13 +980,15 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold // 横屏时隐藏其他UI元素,只显示播放器 binding.topBar.setVisibility(View.GONE); binding.roomInfoLayout.setVisibility(View.GONE); - binding.chatLayout.setVisibility(View.GONE); - binding.exitFullscreenButton.setVisibility(View.GONE); + binding.chatInputLayout.setVisibility(View.GONE); + binding.chatRecyclerView.setVisibility(View.GONE); + binding.exitFullscreenButton.setVisibility(View.VISIBLE); } else { // 竖屏时显示所有UI元素 binding.topBar.setVisibility(View.VISIBLE); binding.roomInfoLayout.setVisibility(View.VISIBLE); - binding.chatLayout.setVisibility(View.VISIBLE); + binding.chatInputLayout.setVisibility(View.VISIBLE); + binding.chatRecyclerView.setVisibility(View.VISIBLE); binding.exitFullscreenButton.setVisibility(View.GONE); } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java index 31457264..5436630a 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java @@ -784,4 +784,102 @@ public interface ApiService { */ @GET("api/front/fan-group/all") Call>>> getAllMyFanGroups(); + + // ==================== 用户活动记录 ==================== + + /** + * 获取观看历史 + */ + @GET("api/front/activity/view-history") + Call>>> getViewHistory( + @Query("type") String type, + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 清除观看历史 + */ + @DELETE("api/front/activity/view-history") + Call> clearViewHistory(@Query("type") String type); + + /** + * 获取点赞记录 + */ + @GET("api/front/activity/like-records") + Call>>> getLikeRecords( + @Query("type") String type, + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取收藏的作品 + */ + @GET("api/front/activity/collected-works") + Call>>> getCollectedWorks( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取关注记录 + */ + @GET("api/front/activity/follow-records") + Call>>> getFollowRecords( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取消费记录 + */ + @GET("api/front/activity/consume-records") + Call>>> getConsumeRecords( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 记录观看历史(新版) + */ + @POST("api/front/activity/record-view") + Call>> recordViewHistoryNew(@Body Map body); + + /** + * 调试Token + */ + @GET("api/front/activity/debug-token") + Call>> debugToken(); + + // ==================== 黑名单接口 ==================== + + /** + * 获取我的黑名单 + */ + @GET("api/front/blacklist/list") + Call>>> getMyBlacklist( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 添加到黑名单 + */ + @POST("api/front/blacklist/add") + Call> addToBlacklist(@Body Map body); + + /** + * 从黑名单移除 + */ + @POST("api/front/blacklist/remove") + Call>> removeFromBlacklist(@Body Map body); + + /** + * 检查是否在黑名单中 + */ + @GET("api/front/blacklist/check/{userId}") + Call>> checkBlacklist(@Path("userId") int userId); + + // ==================== 直播分类 ==================== + + /** + * 获取直播分类列表 + */ + @GET("api/front/live/types") + Call>> getLiveTypes(); } diff --git a/android-app/app/src/main/res/drawable/bg_chat_input_dark.xml b/android-app/app/src/main/res/drawable/bg_chat_input_dark.xml new file mode 100644 index 00000000..7d83631f --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_chat_input_dark.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_broadcast.xml b/android-app/app/src/main/res/layout/activity_broadcast.xml index 1ba7bd5d..872a6a5f 100644 --- a/android-app/app/src/main/res/layout/activity_broadcast.xml +++ b/android-app/app/src/main/res/layout/activity_broadcast.xml @@ -5,15 +5,17 @@ android:layout_height="match_parent" android:background="#000000"> - - + + app:layout_constraintTop_toTopOf="parent" + app:keepAspectRatio="true" + app:aspectRatioMode="adjust" /> - + android:background="#000000"> - - - - - - - - - - - - - - - - - - - - - - + + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#000000"> + + app:show_buffering="when_playing" + app:surface_type="texture_view" /> - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/transparent"> - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app:layout_constraintStart_toStartOf="parent" /> - + + - + + + + + android:layout_height="wrap_content" + android:layout_marginBottom="12dp"> @@ -341,39 +303,120 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" - android:layout_marginBottom="-4dp" + android:layout_marginBottom="-2dp" android:background="@drawable/bg_purple_20" - android:paddingHorizontal="4dp" + android:paddingHorizontal="5dp" android:paddingVertical="1dp" android:text="0" android:textColor="#FFFFFF" - android:textSize="9sp" - android:visibility="visible" /> + android:textSize="9sp" /> + + + + + + + + + + + + + + + + + app:backgroundTint="#FF4081" + app:cornerRadius="19dp" /> + + + @@ -381,20 +424,15 @@ android:id="@+id/loadingIndicator" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="gone" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + android:layout_gravity="center" + android:visibility="gone" /> - + + android:layout_gravity="bottom" /> - \ No newline at end of file + \ No newline at end of file diff --git a/android-app/gradle/wrapper/gradle-wrapper.properties b/android-app/gradle/wrapper/gradle-wrapper.properties index 8b6cf8e6..ed7b921e 100644 --- a/android-app/gradle/wrapper/gradle-wrapper.properties +++ b/android-app/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=file:///D:/soft/gradle-8.1-bin.zip +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.14-bin.zip networkTimeout=600000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists