zhibo/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
2026-01-04 20:50:37 +08:00

2635 lines
109 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.example.livestreaming;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.GridLayoutManager;
import com.example.livestreaming.call.WebRTCConfig;
import com.example.livestreaming.databinding.ActivityRoomDetailNewBinding;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.AuthStore;
import com.example.livestreaming.net.ChatMessageResponse;
import com.example.livestreaming.net.CreateRechargeRequest;
import com.example.livestreaming.net.CreateRechargeResponse;
import com.example.livestreaming.net.GiftResponse;
import com.example.livestreaming.net.OrderPayRequest;
import com.example.livestreaming.net.OrderPayResultResponse;
import com.example.livestreaming.net.RechargeOptionResponse;
import com.example.livestreaming.net.Room;
import com.example.livestreaming.net.SendGiftRequest;
import com.example.livestreaming.net.SendGiftResponse;
import com.example.livestreaming.net.StreamConfig;
import com.example.livestreaming.net.UserBalanceResponse;
import com.example.livestreaming.ShareUtils;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import tv.danmaku.ijk.media.player.IMediaPlayer;
import tv.danmaku.ijk.media.player.IjkMediaPlayer;
// GSYVideoPlayer imports
import com.shuyu.gsyvideoplayer.GSYVideoManager;
import com.shuyu.gsyvideoplayer.listener.GSYSampleCallBack;
import com.shuyu.gsyvideoplayer.utils.GSYVideoType;
import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer;
import com.shuyu.gsyvideoplayer.builder.GSYVideoOptionBuilder;
import com.shuyu.gsyvideoplayer.player.PlayerFactory;
import com.shuyu.gsyvideoplayer.player.IjkPlayerManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
import org.json.JSONException;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import org.json.JSONObject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class RoomDetailActivity extends AppCompatActivity implements SurfaceHolder.Callback {
public static final String EXTRA_ROOM_ID = "extra_room_id";
private ActivityRoomDetailNewBinding binding;
private final Handler handler = new Handler(Looper.getMainLooper());
private Runnable pollRunnable;
private String roomId;
private Room room;
private ExoPlayer player;
private boolean triedAltUrl;
private IjkMediaPlayer ijkPlayer;
private Surface ijkSurface;
private SurfaceHolder ijkSurfaceHolder;
private String ijkUrl;
private String ijkFallbackHlsUrl;
private boolean ijkFallbackTried;
private String pendingFlvUrl;
// GSYVideoPlayer 相关
private StandardGSYVideoPlayer gsyPlayer;
private boolean useGSYPlayer = true; // 默认使用GSYVideoPlayer播放FLV
private static boolean ijkLibLoaded;
private boolean isFullscreen = false;
private ChatAdapter chatAdapter;
private List<ChatMessage> chatMessages = new ArrayList<>();
private Random random = new Random();
// WebSocket - 弹幕
private WebSocket chatWebSocket;
private OkHttpClient chatWsClient;
// WebSocket - 在线人数
private WebSocket onlineCountWebSocket;
private OkHttpClient onlineCountWsClient;
// 动态获取WebSocket URL - 直播服务使用远程服务器
private String getWsChatBaseUrl() {
try {
// 直播弹幕WebSocket使用远程服务器
return WebRTCConfig.getLiveWsUrl() + "ws/live/chat/";
} catch (Exception e) {
android.util.Log.e("RoomDetail", "获取WsChatBaseUrl失败", e);
return "ws://1.15.149.240:8083/ws/live/chat/";
}
}
private String getWsOnlineBaseUrl() {
try {
// 直播在线人数WebSocket使用远程服务器
return WebRTCConfig.getLiveWsUrl() + "ws/live/";
} catch (Exception e) {
android.util.Log.e("RoomDetail", "获取WsOnlineBaseUrl失败", e);
return "ws://1.15.149.240:8083/ws/live/";
}
}
// WebSocket 心跳检测 - 弹幕
private Runnable chatHeartbeatRunnable;
private Runnable chatReconnectRunnable;
private int chatReconnectAttempts = 0;
private boolean isChatWebSocketConnected = false;
// WebSocket 心跳检测 - 在线人数
private Runnable onlineHeartbeatRunnable;
private Runnable onlineReconnectRunnable;
private int onlineReconnectAttempts = 0;
private boolean isOnlineWebSocketConnected = false;
// WebSocket 常量
private static final long HEARTBEAT_INTERVAL = 30000; // 30秒心跳间隔
private static final long RECONNECT_DELAY = 5000; // 5秒重连延迟
private static final int MAX_RECONNECT_ATTEMPTS = 5;
// 礼物相关
private BottomSheetDialog giftDialog;
private GiftAdapter giftAdapter;
private List<Gift> availableGifts;
private int userCoinBalance = 0; // 用户金币余额(从后端加载)
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
android.util.Log.d("RoomDetail", "onCreate开始");
// 隐藏ActionBar使用自定义顶部栏
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
android.util.Log.d("RoomDetail", "开始inflate布局");
binding = ActivityRoomDetailNewBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
android.util.Log.d("RoomDetail", "布局inflate完成");
ApiClient.getService(getApplicationContext());
roomId = getIntent().getStringExtra(EXTRA_ROOM_ID);
android.util.Log.d("RoomDetail", "roomId=" + roomId);
if (TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "直播间ID无效", Toast.LENGTH_SHORT).show();
finish();
return;
}
triedAltUrl = false;
android.util.Log.d("RoomDetail", "开始setupUI");
setupUI();
android.util.Log.d("RoomDetail", "setupUI完成开始setupSurface");
setupSurface();
android.util.Log.d("RoomDetail", "setupSurface完成开始setupChat");
setupChat();
android.util.Log.d("RoomDetail", "setupChat完成开始setupGifts");
setupGifts();
android.util.Log.d("RoomDetail", "setupGifts完成");
// 加载房间信息
android.util.Log.d("RoomDetail", "开始loadRoomInfo");
loadRoomInfo();
android.util.Log.d("RoomDetail", "loadRoomInfo完成");
// 记录观看历史(异步,不阻塞)
recordWatchHistory();
android.util.Log.d("RoomDetail", "onCreate完成");
} catch (Exception e) {
android.util.Log.e("RoomDetail", "onCreate异常: " + e.getMessage(), e);
e.printStackTrace();
Toast.makeText(this, "加载直播间失败: " + e.getMessage(), Toast.LENGTH_LONG).show();
finish();
}
}
private void setupUI() {
// 返回按钮
binding.backButton.setOnClickListener(v -> finish());
binding.exitFullscreenButton.setOnClickListener(v -> {
if (isFullscreen) {
toggleFullscreen();
} else {
finish();
}
});
View.OnClickListener fullscreenOverlayToggle = v -> {
if (!isFullscreen) return;
int vis = binding.exitFullscreenButton.getVisibility();
binding.exitFullscreenButton.setVisibility(vis == View.VISIBLE ? View.GONE : View.VISIBLE);
};
// PlayerView 往往会自己消费触摸事件,导致 container 的点击回调不触发,所以这里两处都绑定
binding.playerContainer.setOnClickListener(fullscreenOverlayToggle);
binding.playerView.setOnClickListener(fullscreenOverlayToggle);
// 全屏按钮
binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen());
// 关注按钮
binding.followButton.setOnClickListener(v -> {
// 检查登录状态,关注主播需要登录
if (!AuthHelper.requireLogin(this, "关注主播需要登录")) {
return;
}
if (room == null) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 调用后端接口关注主播
followStreamerBackend();
});
// 分享按钮
binding.shareButton.setOnClickListener(v -> shareRoom());
}
/**
* 初始化 SurfaceView 用于 IjkPlayer 播放 FLV
*/
private void setupSurface() {
if (binding.flvSurfaceView != null) {
binding.flvSurfaceView.getHolder().addCallback(this);
android.util.Log.d("RoomDetail", "SurfaceView callback 已设置");
}
}
private void setupChat() {
// 设置弹幕适配器
chatAdapter = new ChatAdapter();
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setStackFromEnd(true); // 从底部开始显示
binding.chatRecyclerView.setLayoutManager(layoutManager);
binding.chatRecyclerView.setAdapter(chatAdapter);
// 礼物按钮点击事件
binding.giftButton.setOnClickListener(v -> showGiftDialog());
// 发送按钮点击事件
binding.sendButton.setOnClickListener(v -> sendMessage());
// 点赞按钮点击事件
ImageButton likeButton = findViewById(R.id.likeButton);
TextView likeCountText = findViewById(R.id.likeCountText);
if (likeButton != null && likeCountText != null) {
// 加载点赞数
loadLikeCount();
// 点赞按钮点击事件
likeButton.setOnClickListener(v -> {
if (!AuthHelper.isLoggedIn(this)) {
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
return;
}
// 点赞动画
likeButton.animate()
.scaleX(1.3f)
.scaleY(1.3f)
.setDuration(100)
.withEndAction(() -> {
likeButton.animate()
.scaleX(1.0f)
.scaleY(1.0f)
.setDuration(100)
.start();
})
.start();
// 调用点赞API
likeRoom();
});
}
// 输入框回车发送
binding.chatInput.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEND ||
(event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
sendMessage();
return true;
}
return false;
});
}
/**
* 加载点赞数
*/
private void loadLikeCount() {
if (roomId == null) return;
try {
int roomIdInt = Integer.parseInt(roomId);
ApiClient.getService(this)
.getRoomLikeCount(roomIdInt)
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
Map<String, Object> data = response.body().getData();
if (data != null && data.containsKey("likeCount")) {
int likeCount = ((Number) data.get("likeCount")).intValue();
TextView likeCountText = findViewById(R.id.likeCountText);
if (likeCountText != null) {
likeCountText.setText(String.valueOf(likeCount));
}
}
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
// 忽略错误
}
});
} catch (NumberFormatException e) {
// 忽略错误
}
}
/**
* 点赞直播间
*/
private void likeRoom() {
if (roomId == null) {
android.util.Log.e("RoomDetail", "点赞失败: roomId为空");
Toast.makeText(this, "直播间信息错误", Toast.LENGTH_SHORT).show();
return;
}
try {
int roomIdInt = Integer.parseInt(roomId);
// 立即更新UI先乐观更新
TextView likeCountText = findViewById(R.id.likeCountText);
if (likeCountText != null) {
try {
int currentCount = Integer.parseInt(likeCountText.getText().toString());
likeCountText.setText(String.valueOf(currentCount + 1));
} catch (NumberFormatException e) {
// 如果解析失败从1开始
likeCountText.setText("1");
}
}
Map<String, Object> request = new HashMap<>();
request.put("count", 1);
android.util.Log.d("RoomDetail", "开始点赞: roomId=" + roomIdInt);
ApiClient.getService(this)
.likeRoom(roomIdInt, request)
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
android.util.Log.d("RoomDetail", "点赞响应: code=" + response.code() +
", body=" + (response.body() != null ? response.body().toString() : "null"));
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.isOk()) {
Map<String, Object> data = apiResponse.getData();
if (data != null && data.containsKey("likeCount")) {
// 使用服务器返回的真实点赞数更新UI
int likeCount = ((Number) data.get("likeCount")).intValue();
TextView likeCountText = findViewById(R.id.likeCountText);
if (likeCountText != null) {
likeCountText.setText(String.valueOf(likeCount));
}
}
// 不显示Toast避免打扰用户
} else {
// 如果失败,恢复原来的点赞数
loadLikeCount();
String errorMsg = apiResponse.getMessage();
android.util.Log.e("RoomDetail", "点赞失败: " + errorMsg);
Toast.makeText(RoomDetailActivity.this,
errorMsg != null ? errorMsg : "点赞失败",
Toast.LENGTH_SHORT).show();
}
} else {
// 如果失败,恢复原来的点赞数
loadLikeCount();
android.util.Log.e("RoomDetail", "点赞请求失败: code=" + response.code());
Toast.makeText(RoomDetailActivity.this, "点赞失败", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
// 如果失败,恢复原来的点赞数
loadLikeCount();
android.util.Log.e("RoomDetail", "点赞网络错误: " + t.getMessage(), t);
Toast.makeText(RoomDetailActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
}
});
} catch (NumberFormatException e) {
android.util.Log.e("RoomDetail", "直播间ID格式错误: " + roomId);
Toast.makeText(this, "直播间ID格式错误", Toast.LENGTH_SHORT).show();
}
}
private void sendMessage() {
// 检查登录状态,发送弹幕需要登录
if (!AuthHelper.requireLoginWithToast(this, "发送弹幕需要登录")) {
return;
}
String message = binding.chatInput.getText() != null ?
binding.chatInput.getText().toString().trim() : "";
if (!TextUtils.isEmpty(message)) {
binding.chatInput.setText("");
// 通过 WebSocket 发送消息
sendChatViaWebSocket(message);
}
}
private void connectWebSocket() {
if (TextUtils.isEmpty(roomId)) return;
// 连接弹幕WebSocket
connectChatWebSocket();
// 连接在线人数WebSocket
connectOnlineCountWebSocket();
}
/**
* 连接弹幕WebSocket
*/
private void connectChatWebSocket() {
if (TextUtils.isEmpty(roomId)) return;
// 停止之前的重连任务
stopChatReconnect();
chatWsClient = new OkHttpClient.Builder()
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS) // OkHttp 内置 ping
.build();
Request request = new Request.Builder()
.url(getWsChatBaseUrl() + roomId)
.build();
chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, okhttp3.Response response) {
android.util.Log.d("ChatWebSocket", "弹幕连接成功: roomId=" + roomId);
isChatWebSocketConnected = true;
chatReconnectAttempts = 0;
// 启动心跳检测
startChatHeartbeat();
}
@Override
public void onMessage(WebSocket webSocket, String text) {
// 收到消息,解析并显示
try {
JSONObject json = new JSONObject(text);
String type = json.optString("type", "");
if ("chat".equals(type)) {
String nickname = json.optString("nickname", "匿名");
String content = json.optString("content", "");
handler.post(() -> {
addChatMessage(new ChatMessage(nickname, content));
});
} else if ("gift".equals(type)) {
// 处理礼物消息
String giftName = json.optString("giftName", "");
int count = json.optInt("count", 1);
String senderNickname = json.optString("senderNickname", "匿名");
int totalPrice = json.optInt("totalPrice", 0);
handler.post(() -> {
String giftMsg = senderNickname + " 送出了 " + count + "" + giftName;
addChatMessage(new ChatMessage(giftMsg, true)); // 系统消息样式
// TODO: 显示礼物动画效果
showGiftAnimation(giftName, count, senderNickname);
});
} else if ("gift_combo".equals(type)) {
// 处理礼物连击消息
String giftName = json.optString("giftName", "");
int comboCount = json.optInt("comboCount", 1);
String senderNickname = json.optString("senderNickname", "匿名");
handler.post(() -> {
String comboMsg = senderNickname + "" + giftName + " 连击 x" + comboCount;
addChatMessage(new ChatMessage(comboMsg, true));
});
} else if ("connected".equals(type)) {
String content = json.optString("content", "");
handler.post(() -> {
addChatMessage(new ChatMessage(content, true));
});
} else if ("system".equals(type)) {
// 处理系统消息
String content = json.optString("content", "");
handler.post(() -> {
addChatMessage(new ChatMessage(content, true));
});
} else if ("pong".equals(type)) {
// 收到心跳响应
android.util.Log.d("ChatWebSocket", "收到心跳响应");
}
} catch (JSONException e) {
android.util.Log.e("ChatWebSocket", "解析消息失败: " + e.getMessage());
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) {
android.util.Log.e("ChatWebSocket", "连接失败: " + t.getMessage());
isChatWebSocketConnected = false;
stopChatHeartbeat();
// 尝试重连
scheduleChatReconnect();
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
android.util.Log.d("ChatWebSocket", "连接关闭: " + reason);
isChatWebSocketConnected = false;
stopChatHeartbeat();
// 非正常关闭时尝试重连
if (code != 1000) {
scheduleChatReconnect();
}
}
});
}
/**
* 连接在线人数WebSocket
*/
private void connectOnlineCountWebSocket() {
if (TextUtils.isEmpty(roomId)) return;
// 停止之前的重连任务
stopOnlineReconnect();
// 构建WebSocket URL添加clientId参数
String userIdStr = AuthStore.getUserId(this);
String clientId = (userIdStr != null && !userIdStr.isEmpty()) ?
userIdStr :
"guest_" + System.currentTimeMillis();
String wsUrl = getWsOnlineBaseUrl() + roomId + "?clientId=" + clientId;
onlineCountWsClient = new OkHttpClient.Builder()
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url(wsUrl)
.build();
onlineCountWebSocket = onlineCountWsClient.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, okhttp3.Response response) {
android.util.Log.d("OnlineWebSocket", "在线人数连接成功: roomId=" + roomId);
isOnlineWebSocketConnected = true;
onlineReconnectAttempts = 0;
// 启动心跳检测
startOnlineHeartbeat();
}
@Override
public void onMessage(WebSocket webSocket, String text) {
// 收到在线人数更新消息
try {
JSONObject json = new JSONObject(text);
String type = json.optString("type", "");
if ("online_count".equals(type)) {
int count = json.optInt("count", 0);
android.util.Log.d("OnlineWebSocket", "收到在线人数更新: " + count);
handler.post(() -> {
// 更新UI显示在线人数
if (binding != null && binding.topViewerCount != null) {
binding.topViewerCount.setText(String.valueOf(count));
}
});
} else if ("connected".equals(type)) {
String message = json.optString("message", "");
android.util.Log.d("OnlineWebSocket", "连接确认: " + message);
} else if ("pong".equals(type)) {
// 收到心跳响应
android.util.Log.d("OnlineWebSocket", "收到心跳响应");
}
} catch (JSONException e) {
android.util.Log.e("OnlineWebSocket", "解析消息失败: " + e.getMessage());
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) {
android.util.Log.e("OnlineWebSocket", "连接失败: " + t.getMessage());
isOnlineWebSocketConnected = false;
stopOnlineHeartbeat();
// 尝试重连
scheduleOnlineReconnect();
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
android.util.Log.d("OnlineWebSocket", "连接关闭: " + reason);
isOnlineWebSocketConnected = false;
stopOnlineHeartbeat();
// 非正常关闭时尝试重连
if (code != 1000) {
scheduleOnlineReconnect();
}
}
});
}
/**
* 启动弹幕心跳检测
*/
private void startChatHeartbeat() {
stopChatHeartbeat();
chatHeartbeatRunnable = new Runnable() {
@Override
public void run() {
if (chatWebSocket != null && isChatWebSocketConnected) {
try {
JSONObject ping = new JSONObject();
ping.put("type", "ping");
chatWebSocket.send(ping.toString());
android.util.Log.d("ChatWebSocket", "发送心跳");
} catch (JSONException e) {
android.util.Log.e("ChatWebSocket", "发送心跳失败: " + e.getMessage());
}
handler.postDelayed(chatHeartbeatRunnable, HEARTBEAT_INTERVAL);
}
}
};
handler.postDelayed(chatHeartbeatRunnable, HEARTBEAT_INTERVAL);
}
/**
* 停止弹幕心跳检测
*/
private void stopChatHeartbeat() {
if (chatHeartbeatRunnable != null) {
handler.removeCallbacks(chatHeartbeatRunnable);
chatHeartbeatRunnable = null;
}
}
/**
* 启动在线人数心跳检测
*/
private void startOnlineHeartbeat() {
stopOnlineHeartbeat();
onlineHeartbeatRunnable = new Runnable() {
@Override
public void run() {
if (onlineCountWebSocket != null && isOnlineWebSocketConnected) {
try {
JSONObject ping = new JSONObject();
ping.put("type", "ping");
onlineCountWebSocket.send(ping.toString());
android.util.Log.d("OnlineWebSocket", "发送心跳");
} catch (JSONException e) {
android.util.Log.e("OnlineWebSocket", "发送心跳失败: " + e.getMessage());
}
handler.postDelayed(onlineHeartbeatRunnable, HEARTBEAT_INTERVAL);
}
}
};
handler.postDelayed(onlineHeartbeatRunnable, HEARTBEAT_INTERVAL);
}
/**
* 停止在线人数心跳检测
*/
private void stopOnlineHeartbeat() {
if (onlineHeartbeatRunnable != null) {
handler.removeCallbacks(onlineHeartbeatRunnable);
onlineHeartbeatRunnable = null;
}
}
/**
* 安排弹幕重连
*/
private void scheduleChatReconnect() {
if (isFinishing() || isDestroyed()) return;
if (chatReconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
android.util.Log.w("ChatWebSocket", "达到最大重连次数,停止重连");
handler.post(() -> {
addChatMessage(new ChatMessage("弹幕连接断开,请刷新页面重试", true));
});
return;
}
chatReconnectAttempts++;
long delay = RECONNECT_DELAY * chatReconnectAttempts; // 递增延迟
android.util.Log.d("ChatWebSocket", "将在 " + delay + "ms 后尝试第 " + chatReconnectAttempts + " 次重连");
chatReconnectRunnable = () -> {
if (!isFinishing() && !isDestroyed()) {
connectChatWebSocket();
}
};
handler.postDelayed(chatReconnectRunnable, delay);
}
/**
* 停止弹幕重连
*/
private void stopChatReconnect() {
if (chatReconnectRunnable != null) {
handler.removeCallbacks(chatReconnectRunnable);
chatReconnectRunnable = null;
}
}
/**
* 安排在线人数重连
*/
private void scheduleOnlineReconnect() {
if (isFinishing() || isDestroyed()) return;
if (onlineReconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
android.util.Log.w("OnlineWebSocket", "达到最大重连次数,停止重连");
return;
}
onlineReconnectAttempts++;
long delay = RECONNECT_DELAY * onlineReconnectAttempts; // 递增延迟
android.util.Log.d("OnlineWebSocket", "将在 " + delay + "ms 后尝试第 " + onlineReconnectAttempts + " 次重连");
onlineReconnectRunnable = () -> {
if (!isFinishing() && !isDestroyed()) {
connectOnlineCountWebSocket();
}
};
handler.postDelayed(onlineReconnectRunnable, delay);
}
/**
* 停止在线人数重连
*/
private void stopOnlineReconnect() {
if (onlineReconnectRunnable != null) {
handler.removeCallbacks(onlineReconnectRunnable);
onlineReconnectRunnable = null;
}
}
private void sendChatViaWebSocket(String content) {
if (chatWebSocket == null || !isChatWebSocketConnected) {
// 如果 WebSocket 未连接,先本地显示
addChatMessage(new ChatMessage("", content));
Toast.makeText(this, "弹幕连接断开,消息仅本地显示", Toast.LENGTH_SHORT).show();
return;
}
try {
JSONObject json = new JSONObject();
json.put("type", "chat");
json.put("content", content);
json.put("nickname", AuthStore.getNickname(this));
json.put("userId", AuthStore.getUserId(this));
chatWebSocket.send(json.toString());
} catch (JSONException e) {
android.util.Log.e("ChatWebSocket", "发送消息失败: " + e.getMessage());
// 失败时本地显示
addChatMessage(new ChatMessage("", content));
}
}
private void disconnectWebSocket() {
// 断开弹幕WebSocket
stopChatHeartbeat();
stopChatReconnect();
isChatWebSocketConnected = false;
if (chatWebSocket != null) {
chatWebSocket.close(1000, "Activity destroyed");
chatWebSocket = null;
}
if (chatWsClient != null) {
chatWsClient.dispatcher().executorService().shutdown();
chatWsClient = null;
}
// 断开在线人数WebSocket
stopOnlineHeartbeat();
stopOnlineReconnect();
isOnlineWebSocketConnected = false;
if (onlineCountWebSocket != null) {
onlineCountWebSocket.close(1000, "Activity destroyed");
onlineCountWebSocket = null;
}
if (onlineCountWsClient != null) {
onlineCountWsClient.dispatcher().executorService().shutdown();
onlineCountWsClient = null;
}
}
private void addChatMessage(ChatMessage message) {
// 限制消息数量,防止内存溢出
if (chatMessages.size() > 100) {
chatMessages.remove(0);
}
chatMessages.add(message);
chatAdapter.submitList(new ArrayList<>(chatMessages));
// 滚动到最新消息
if (binding != null && binding.chatRecyclerView != null && chatMessages.size() > 0) {
binding.chatRecyclerView.scrollToPosition(chatMessages.size() - 1);
}
}
private void toggleFullscreen() {
if (isFullscreen) {
// 退出全屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
// ActionBar可能为null需要检查
androidx.appcompat.app.ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.show();
}
isFullscreen = false;
if (binding != null) binding.exitFullscreenButton.setVisibility(View.GONE);
} else {
// 进入全屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
// ActionBar可能为null需要检查
androidx.appcompat.app.ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
isFullscreen = true;
if (binding != null) binding.exitFullscreenButton.setVisibility(View.GONE);
}
}
@Override
protected void onStart() {
super.onStart();
startPolling();
connectWebSocket(); // 连接 WebSocket
loadHistoryMessages(); // 加载历史弹幕
}
@Override
protected void onStop() {
super.onStop();
stopPolling();
disconnectWebSocket(); // 断开 WebSocket
releasePlayer();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 横屏时隐藏其他UI元素只显示播放器
binding.topBar.setVisibility(View.GONE);
binding.roomInfoLayout.setVisibility(View.GONE);
binding.chatLayout.setVisibility(View.GONE);
binding.exitFullscreenButton.setVisibility(View.GONE);
} else {
// 竖屏时显示所有UI元素
binding.topBar.setVisibility(View.VISIBLE);
binding.roomInfoLayout.setVisibility(View.VISIBLE);
binding.chatLayout.setVisibility(View.VISIBLE);
binding.exitFullscreenButton.setVisibility(View.GONE);
}
}
private void startPolling() {
stopPolling();
// 首次立即获取房间信息
fetchRoom();
pollRunnable = () -> {
if (!isFinishing() && !isDestroyed()) {
fetchRoom();
handler.postDelayed(pollRunnable, 15000); // 15秒轮询一次减少压力
}
};
// 延迟15秒后开始轮询
handler.postDelayed(pollRunnable, 15000);
}
private void stopPolling() {
if (pollRunnable != null) {
handler.removeCallbacks(pollRunnable);
pollRunnable = null;
}
}
/**
* 记录观看历史
*/
private void recordWatchHistory() {
try {
if (!AuthHelper.isLoggedIn(this)) {
return; // 未登录用户不记录
}
if (TextUtils.isEmpty(roomId)) {
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("roomId", roomId);
body.put("watchTime", System.currentTimeMillis());
Call<ApiResponse<java.util.Map<String, Object>>> call = apiService.recordWatchHistory(body);
call.enqueue(new Callback<ApiResponse<java.util.Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call,
Response<ApiResponse<java.util.Map<String, Object>>> response) {
// 忽略结果,接口可能不存在
}
@Override
public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
// 忽略错误,接口可能不存在
}
});
} catch (Exception e) {
// 忽略所有异常,不影响直播观看
android.util.Log.w("RoomDetail", "记录观看历史失败: " + e.getMessage());
}
}
/**
* 加载历史弹幕消息
*/
private void loadHistoryMessages() {
if (TextUtils.isEmpty(roomId)) return;
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<List<ChatMessageResponse>>> call = apiService.getRoomMessages(roomId, 50);
call.enqueue(new Callback<ApiResponse<List<ChatMessageResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<List<ChatMessageResponse>>> call,
Response<ApiResponse<List<ChatMessageResponse>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<List<ChatMessageResponse>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
List<ChatMessageResponse> messages = apiResponse.getData();
// 添加欢迎消息
if (messages.isEmpty()) {
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
}
// 添加历史消息
for (ChatMessageResponse msg : messages) {
String nickname = msg.getNickname();
String content = msg.getContent();
if (nickname != null && content != null) {
addChatMessage(new ChatMessage(nickname, content));
}
}
android.util.Log.d("RoomDetail", "加载了 " + messages.size() + " 条历史弹幕");
} else {
// 如果加载失败,显示欢迎消息
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
}
} else {
// 如果加载失败,显示欢迎消息
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
}
}
@Override
public void onFailure(Call<ApiResponse<List<ChatMessageResponse>>> call, Throwable t) {
android.util.Log.e("RoomDetail", "加载历史弹幕失败: " + t.getMessage());
// 如果加载失败,显示欢迎消息
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
}
});
}
private boolean isFirstLoad = true;
private void fetchRoom() {
android.util.Log.d("RoomDetail", "fetchRoom() roomId=" + roomId);
if (TextUtils.isEmpty(roomId)) {
android.util.Log.e("RoomDetail", "roomId 为空,退出");
Toast.makeText(this, "房间不存在", Toast.LENGTH_SHORT).show();
finish();
return;
}
// 只在首次加载时显示loading
if (isFirstLoad) {
binding.loadingIndicator.setVisibility(View.VISIBLE);
}
ApiClient.getService(getApplicationContext()).getRoom(roomId).enqueue(new Callback<ApiResponse<Room>>() {
@Override
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
if (isFinishing() || isDestroyed()) return;
binding.loadingIndicator.setVisibility(View.GONE);
boolean firstLoad = isFirstLoad;
isFirstLoad = false;
android.util.Log.d("RoomDetail", "onResponse: code=" + response.code() + ", firstLoad=" + firstLoad);
ApiResponse<Room> body = response.body();
Room data = body != null ? body.getData() : null;
android.util.Log.d("RoomDetail", "body=" + (body != null) + ", isOk=" + (body != null && body.isOk()) + ", data=" + (data != null));
if (!response.isSuccessful() || body == null || !body.isOk() || data == null) {
String msg = body != null && !TextUtils.isEmpty(body.getMessage()) ? body.getMessage() : "房间不存在";
android.util.Log.e("RoomDetail", "API 失败: " + msg);
// 不要在首次加载失败时退出,让用户可以看到错误
Toast.makeText(RoomDetailActivity.this, msg, Toast.LENGTH_SHORT).show();
return;
}
room = data;
android.util.Log.d("RoomDetail", "房间加载成功: " + room.getTitle());
bindRoom(room);
}
@Override
public void onFailure(Call<ApiResponse<Room>> call, Throwable t) {
if (isFinishing() || isDestroyed()) return;
binding.loadingIndicator.setVisibility(View.GONE);
isFirstLoad = false;
android.util.Log.e("RoomDetail", "onFailure: " + t.getMessage());
}
});
}
private void shareRoom() {
if (room == null || roomId == null) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
String shareLink = ShareUtils.generateRoomShareLink(roomId);
String title = room.getTitle() != null ? room.getTitle() : "直播间";
String text = "来看看这个精彩的直播:" + title;
ShareUtils.shareLink(this, shareLink, title, text);
}
private void bindRoom(Room r) {
try {
String title = r.getTitle() != null ? r.getTitle() : "直播间";
String streamer = r.getStreamerName() != null ? r.getStreamerName() : "主播";
// 设置顶部标题栏
binding.topTitle.setText(title);
// 设置房间信息区域
binding.roomTitle.setText(title);
binding.streamerName.setText(streamer);
// 设置直播状态
if (r.isLive()) {
binding.liveTag.setVisibility(View.VISIBLE);
binding.offlineLayout.setVisibility(View.GONE);
// 在线人数已通过WebSocket实时更新这里只设置初始值
if (r.getViewerCount() > 0) {
binding.topViewerCount.setText(String.valueOf(r.getViewerCount()));
}
// 如果后端返回的viewerCount为0保持UI不变等待WebSocket推送
} else {
binding.liveTag.setVisibility(View.GONE);
binding.offlineLayout.setVisibility(View.VISIBLE);
releasePlayer();
return;
}
// 获取播放地址
// 优先使用 HTTP-FLVIjkPlayer延迟更低2-3秒
// HLS 作为备用(延迟 6-10秒
String playUrl = null;
String fallbackUrl = null;
if (r.getStreamUrls() != null) {
// 打印流地址信息用于调试
android.util.Log.d("RoomDetail", "流地址信息:");
android.util.Log.d("RoomDetail", " FLV: " + r.getStreamUrls().getFlv());
android.util.Log.d("RoomDetail", " HLS: " + r.getStreamUrls().getHls());
android.util.Log.d("RoomDetail", " RTMP: " + r.getStreamUrls().getRtmp());
// 优先使用 FLV低延迟
playUrl = r.getStreamUrls().getFlv();
fallbackUrl = r.getStreamUrls().getHls();
// 如果 FLV 不可用,使用 HLS
if (TextUtils.isEmpty(playUrl)) {
android.util.Log.w("RoomDetail", "FLV 地址为空,使用 HLS");
playUrl = fallbackUrl;
fallbackUrl = null;
}
} else {
android.util.Log.e("RoomDetail", "streamUrls 为 null");
}
android.util.Log.d("RoomDetail", "最终播放地址: " + playUrl);
android.util.Log.d("RoomDetail", "备用地址: " + fallbackUrl);
if (!TextUtils.isEmpty(playUrl)) {
ensurePlayer(playUrl, fallbackUrl);
} else {
// 没有播放地址时显示离线状态
android.util.Log.e("RoomDetail", "没有可用的播放地址,显示离线状态");
binding.offlineLayout.setVisibility(View.VISIBLE);
releasePlayer();
}
} catch (Exception e) {
android.util.Log.e("RoomDetail", "bindRoom异常: " + e.getMessage(), e);
// 不要因为绑定失败就退出,显示错误状态
if (binding != null && binding.offlineLayout != null) {
binding.offlineLayout.setVisibility(View.VISIBLE);
}
}
}
private void ensurePlayer(String url, String fallbackHlsUrl) {
try {
if (TextUtils.isEmpty(url)) return;
if (url.endsWith(".flv")) {
if (TextUtils.equals(ijkUrl, url) && ijkPlayer != null) return;
startFlv(url, fallbackHlsUrl);
return;
}
if (player != null) {
MediaItem current = player.getCurrentMediaItem();
String currentUri = current != null && current.localConfiguration != null && current.localConfiguration.uri != null
? current.localConfiguration.uri.toString()
: null;
if (currentUri != null && currentUri.equals(url)) return;
}
startHls(url, null);
} catch (Exception e) {
android.util.Log.e("RoomDetail", "ensurePlayer异常: " + e.getMessage(), e);
// 播放器初始化失败,显示离线状态
if (binding != null && binding.offlineLayout != null) {
binding.offlineLayout.setVisibility(View.VISIBLE);
}
}
}
// 防止重复显示连接消息
private boolean hasShownConnectedMessage = false;
private void startHls(String url, @Nullable String altUrl) {
releaseIjkPlayer();
if (binding != null) {
binding.flvTextureView.setVisibility(View.GONE);
binding.playerView.setVisibility(View.VISIBLE);
}
releaseExoPlayer();
triedAltUrl = false;
hasShownConnectedMessage = false; // 重置连接消息标志
// 优化缓冲配置 - 针对低延迟 HLS1秒分片
// 平衡延迟和流畅度
androidx.media3.exoplayer.DefaultLoadControl loadControl =
new androidx.media3.exoplayer.DefaultLoadControl.Builder()
.setBufferDurationsMs(
2500, // 最小缓冲 2.5秒约2-3个分片
10000, // 最大缓冲 10秒
1500, // 播放前缓冲 1.5秒(快速起播)
2500 // 重新缓冲 2.5秒
)
.setPrioritizeTimeOverSizeThresholds(true)
.build();
// 创建播放器
ExoPlayer exo = new ExoPlayer.Builder(this)
.setLoadControl(loadControl)
.build();
// 关键:设置为直播模式,自动跳到最新位置
exo.setPlayWhenReady(true);
// 设置播放器视图
binding.playerView.setPlayer(exo);
binding.playerView.setUseController(true);
binding.playerView.setControllerAutoShow(false);
String computedAltUrl = altUrl;
if (TextUtils.isEmpty(computedAltUrl)) computedAltUrl = getAltHlsUrl(url);
String finalComputedAltUrl = computedAltUrl;
final String finalUrl = url;
exo.addListener(new Player.Listener() {
private int retryCount = 0;
private static final int MAX_RETRY = 3;
@Override
public void onPlayerError(PlaybackException error) {
android.util.Log.e("ExoPlayer", "播放错误: " + error.getMessage());
// 先尝试备用地址
if (!triedAltUrl && !TextUtils.isEmpty(finalComputedAltUrl)) {
triedAltUrl = true;
android.util.Log.d("ExoPlayer", "尝试备用地址: " + finalComputedAltUrl);
exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl));
exo.prepare();
exo.setPlayWhenReady(true);
return;
}
// 自动重试
if (retryCount < MAX_RETRY) {
retryCount++;
android.util.Log.d("ExoPlayer", "自动重试 " + retryCount + "/" + MAX_RETRY);
handler.postDelayed(() -> {
if (!isFinishing() && !isDestroyed()) {
exo.setMediaItem(MediaItem.fromUri(finalUrl));
exo.prepare();
exo.setPlayWhenReady(true);
}
}, 2000);
return;
}
// 重试失败,显示离线状态
binding.offlineLayout.setVisibility(View.VISIBLE);
handler.postDelayed(() -> {
if (!isFinishing() && !isDestroyed()) {
fetchRoom();
}
}, 5000);
}
@Override
public void onPlaybackStateChanged(int playbackState) {
if (playbackState == Player.STATE_READY) {
binding.offlineLayout.setVisibility(View.GONE);
retryCount = 0;
// 关键:跳到直播流的最新位置,减少延迟
if (exo.isCurrentMediaItemLive()) {
exo.seekToDefaultPosition();
}
// 只显示一次连接消息
if (!hasShownConnectedMessage) {
hasShownConnectedMessage = true;
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
}
} else if (playbackState == Player.STATE_BUFFERING) {
android.util.Log.d("ExoPlayer", "正在缓冲...");
}
}
});
android.util.Log.d("ExoPlayer", "开始播放: " + url);
exo.setMediaItem(MediaItem.fromUri(url));
exo.prepare();
exo.setPlayWhenReady(true);
player = exo;
}
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
android.util.Log.d("RoomDetail", "播放FLV流: " + flvUrl);
// 释放其他播放器
releaseExoPlayer();
releaseGSYPlayer();
// 保存备用地址
ijkFallbackHlsUrl = fallbackHlsUrl;
ijkFallbackTried = false;
hasShownConnectedMessage = false;
// 显示 FLV 播放视图(使用 SurfaceView
if (binding != null) {
binding.playerView.setVisibility(View.GONE);
binding.flvTextureView.setVisibility(View.GONE);
binding.flvSurfaceView.setVisibility(View.VISIBLE);
}
// 使用 IjkPlayer 播放 FLV
pendingFlvUrl = flvUrl;
ijkUrl = flvUrl;
// 如果 SurfaceHolder 已经准备好,直接播放
if (ijkSurfaceHolder != null) {
prepareIjkWithHolder(flvUrl, ijkSurfaceHolder);
} else {
android.util.Log.d("RoomDetail", "等待 SurfaceHolder 准备...");
// SurfaceHolder 会在 surfaceCreated 回调中触发播放
}
}
/**
* 释放 GSYVideoPlayer
*/
private void releaseGSYPlayer() {
if (gsyPlayer != null) {
gsyPlayer.release();
}
GSYVideoManager.releaseAllVideos();
}
// SurfaceHolder.Callback 实现
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
android.util.Log.d("IjkPlayer", "SurfaceView created");
ijkSurfaceHolder = holder;
if (pendingFlvUrl != null) {
prepareIjkWithHolder(pendingFlvUrl, holder);
}
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
android.util.Log.d("IjkPlayer", "SurfaceView changed: " + width + "x" + height);
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
android.util.Log.d("IjkPlayer", "SurfaceView destroyed");
ijkSurfaceHolder = null;
}
private void prepareIjkWithHolder(String url, SurfaceHolder holder) {
if (holder == null) return;
IjkMediaPlayer p = new IjkMediaPlayer();
// ========== 关键:视频解码器设置 ==========
// 优先使用软解码(更稳定)
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 0);
// 视频输出格式
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32);
// 开启视频
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "vn", 0);
// ========== 缓冲和延迟优化 ==========
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1000000);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024 * 16);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);
p.setOnPreparedListener(mp -> {
android.util.Log.d("IjkPlayer", "准备完成,开始播放");
binding.offlineLayout.setVisibility(View.GONE);
mp.start();
if (!hasShownConnectedMessage) {
hasShownConnectedMessage = true;
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
}
});
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
android.util.Log.e("IjkPlayer", "播放错误: what=" + what + ", extra=" + extra);
if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) {
binding.offlineLayout.setVisibility(View.VISIBLE);
return true;
}
ijkFallbackTried = true;
startHls(ijkFallbackHlsUrl, null);
return true;
});
p.setOnInfoListener((mp, what, extra) -> {
if (what == IMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
android.util.Log.d("IjkPlayer", "视频开始渲染");
binding.offlineLayout.setVisibility(View.GONE);
}
return false;
});
ijkPlayer = p;
try {
p.setDisplay(holder);
p.setDataSource(url);
android.util.Log.d("IjkPlayer", "开始准备播放: " + url);
p.prepareAsync();
} catch (Exception e) {
android.util.Log.e("IjkPlayer", "播放器初始化失败: " + e.getMessage());
if (!TextUtils.isEmpty(ijkFallbackHlsUrl)) {
startHls(ijkFallbackHlsUrl, null);
}
}
}
private void prepareIjk(String url) {
if (ijkSurface == null) return;
IjkMediaPlayer p = new IjkMediaPlayer();
// ========== 关键:视频解码器设置 ==========
// 使用硬件解码MediaCodec
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 1);
// 如果硬解失败,自动回退到软解
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-hevc", 1);
// 视频输出格式 - 使用 OpenGL ES 渲染
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32);
// 开启视频
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "vn", 0);
// ========== 缓冲和延迟优化 ==========
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 100000);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 5);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min_frames", 50);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 0);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_streamed", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_delay_max", 5);
// ========== 音视频同步 ==========
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "sync-av-start", 0);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 0);
p.setOnPreparedListener(mp -> {
binding.offlineLayout.setVisibility(View.GONE);
mp.start();
// 只显示一次连接消息
if (!hasShownConnectedMessage) {
hasShownConnectedMessage = true;
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
}
});
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
android.util.Log.e("IjkPlayer", "播放错误: what=" + what + ", extra=" + extra);
if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) {
binding.offlineLayout.setVisibility(View.VISIBLE);
handler.postDelayed(() -> {
if (!isFinishing() && !isDestroyed() && room != null && room.isLive()) {
fetchRoom();
}
}, 5000);
return true;
}
ijkFallbackTried = true;
startHls(ijkFallbackHlsUrl, null);
return true;
});
// 添加缓冲监听
p.setOnInfoListener((mp, what, extra) -> {
if (what == IMediaPlayer.MEDIA_INFO_BUFFERING_START) {
android.util.Log.d("IjkPlayer", "开始缓冲...");
// 可以显示加载指示器
} else if (what == IMediaPlayer.MEDIA_INFO_BUFFERING_END) {
android.util.Log.d("IjkPlayer", "缓冲结束");
// 隐藏加载指示器
} else if (what == IMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
android.util.Log.d("IjkPlayer", "视频开始渲染");
binding.offlineLayout.setVisibility(View.GONE);
}
return false;
});
ijkPlayer = p;
try {
p.setSurface(ijkSurface);
p.setDataSource(url);
p.prepareAsync();
} catch (Exception e) {
android.util.Log.e("IjkPlayer", "播放器初始化失败: " + e.getMessage());
if (!TextUtils.isEmpty(ijkFallbackHlsUrl)) {
startHls(ijkFallbackHlsUrl, null);
} else {
binding.offlineLayout.setVisibility(View.VISIBLE);
}
}
}
private static boolean ijkLibLoadFailed = false;
private static void ensureIjkLibsLoaded() {
if (ijkLibLoaded || ijkLibLoadFailed) {
android.util.Log.d("IjkPlayer", "IjkPlayer 库状态: loaded=" + ijkLibLoaded + ", failed=" + ijkLibLoadFailed);
return;
}
try {
// 检查设备 CPU 架构
String[] abis = android.os.Build.SUPPORTED_ABIS;
android.util.Log.d("IjkPlayer", "设备 CPU 架构: " + java.util.Arrays.toString(abis));
boolean supported = false;
for (String abi : abis) {
if ("armeabi-v7a".equals(abi) || "arm64-v8a".equals(abi) || "x86".equals(abi) || "x86_64".equals(abi)) {
supported = true;
break;
}
}
if (!supported) {
android.util.Log.w("IjkPlayer", "设备 CPU 架构不支持 IjkPlayer: " + java.util.Arrays.toString(abis));
ijkLibLoadFailed = true;
return;
}
IjkMediaPlayer.loadLibrariesOnce(null);
IjkMediaPlayer.native_profileBegin("libijkplayer.so");
ijkLibLoaded = true;
android.util.Log.d("IjkPlayer", "IjkPlayer 库加载成功");
} catch (Throwable e) {
android.util.Log.e("IjkPlayer", "加载IjkPlayer库失败: " + e.getMessage(), e);
ijkLibLoadFailed = true;
}
}
private void releasePlayer() {
releaseExoPlayer();
releaseIjkPlayer();
releaseGSYPlayer();
pendingFlvUrl = null;
if (binding != null) {
binding.playerView.setPlayer(null);
binding.flvTextureView.setVisibility(View.GONE);
binding.flvSurfaceView.setVisibility(View.GONE);
binding.playerView.setVisibility(View.VISIBLE);
binding.gsyPlayer.setVisibility(View.GONE);
}
}
private void releaseExoPlayer() {
if (player != null) {
player.release();
player = null;
}
}
private void releaseIjkPlayer() {
if (ijkPlayer != null) {
try {
ijkPlayer.stop();
ijkPlayer.reset();
ijkPlayer.release();
} catch (Exception e) {
android.util.Log.e("IjkPlayer", "释放播放器失败: " + e.getMessage());
}
ijkPlayer = null;
}
if (ijkSurface != null) {
ijkSurface.release();
ijkSurface = null;
}
ijkSurfaceHolder = null;
ijkUrl = null;
ijkFallbackHlsUrl = null;
ijkFallbackTried = false;
}
private String getAltHlsUrl(String url) {
if (url == null) return null;
if (!url.endsWith(".m3u8")) return null;
if (url.contains("/index.m3u8")) return null;
return url.substring(0, url.length() - ".m3u8".length()) + "/index.m3u8";
}
@Override
protected void onPause() {
super.onPause();
// GSYVideoPlayer 暂停
if (gsyPlayer != null) {
gsyPlayer.onVideoPause();
}
}
@Override
protected void onResume() {
super.onResume();
// GSYVideoPlayer 恢复
if (gsyPlayer != null) {
gsyPlayer.onVideoResume();
}
}
@Override
public void onBackPressed() {
// GSYVideoPlayer 返回处理
if (GSYVideoManager.backFromWindowFull(this)) {
return;
}
super.onBackPressed();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 确保Handler回调被清理防止内存泄漏
if (handler != null) {
if (pollRunnable != null) {
handler.removeCallbacks(pollRunnable);
}
// 清理弹幕心跳和重连回调
if (chatHeartbeatRunnable != null) {
handler.removeCallbacks(chatHeartbeatRunnable);
}
if (chatReconnectRunnable != null) {
handler.removeCallbacks(chatReconnectRunnable);
}
// 清理在线人数心跳和重连回调
if (onlineHeartbeatRunnable != null) {
handler.removeCallbacks(onlineHeartbeatRunnable);
}
if (onlineReconnectRunnable != null) {
handler.removeCallbacks(onlineReconnectRunnable);
}
}
// 断开 WebSocket 连接
disconnectWebSocket();
// 释放播放器资源
releasePlayer();
// 释放礼物弹窗
if (giftDialog != null && giftDialog.isShowing()) {
giftDialog.dismiss();
}
}
/**
* 初始化礼物列表
*/
private void setupGifts() {
// 从后端加载礼物列表
loadGiftsFromBackend();
}
/**
* 从后端加载礼物列表
*/
private void loadGiftsFromBackend() {
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<List<GiftResponse>>> call = apiService.getGiftList();
call.enqueue(new Callback<ApiResponse<List<GiftResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<List<GiftResponse>>> call,
Response<ApiResponse<List<GiftResponse>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<List<GiftResponse>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
availableGifts = new ArrayList<>();
for (GiftResponse giftResponse : apiResponse.getData()) {
Gift gift = new Gift(
String.valueOf(giftResponse.getId()),
giftResponse.getName(),
giftResponse.getPrice().intValue(),
R.drawable.ic_gift_rose, // 默认图标实际应从URL加载
giftResponse.getLevel() != null ? giftResponse.getLevel() : 1
);
availableGifts.add(gift);
}
android.util.Log.d("RoomDetail", "成功加载 " + availableGifts.size() + " 个礼物");
} else {
android.util.Log.w("RoomDetail", "加载礼物列表失败: " + apiResponse.getMessage());
setDefaultGifts();
}
} else {
android.util.Log.w("RoomDetail", "加载礼物列表失败");
setDefaultGifts();
}
}
@Override
public void onFailure(Call<ApiResponse<List<GiftResponse>>> call, Throwable t) {
android.util.Log.e("RoomDetail", "加载礼物列表失败: " + t.getMessage());
setDefaultGifts();
}
});
}
/**
* 设置默认礼物列表(当接口失败时使用)
*/
private void setDefaultGifts() {
availableGifts = new ArrayList<>();
// 不再使用模拟数据,只从后端接口获取真实礼物数据
}
/**
* 显示礼物选择弹窗
*/
private void showGiftDialog() {
// 检查登录状态
if (!AuthHelper.requireLoginWithToast(this, "送礼物需要登录")) {
return;
}
if (giftDialog == null) {
giftDialog = new BottomSheetDialog(this);
View view = getLayoutInflater().inflate(R.layout.bottom_sheet_gift, null);
giftDialog.setContentView(view);
// 初始化礼物列表
androidx.recyclerview.widget.RecyclerView giftRecyclerView = view.findViewById(R.id.giftRecyclerView);
giftRecyclerView.setLayoutManager(new GridLayoutManager(this, 4));
giftAdapter = new GiftAdapter();
giftAdapter.setGifts(availableGifts);
giftRecyclerView.setAdapter(giftAdapter);
// 金币余额
android.widget.TextView coinBalance = view.findViewById(R.id.coinBalance);
// 从后端加载用户金币余额
loadUserBalance(coinBalance);
// 选中的礼物信息
android.widget.LinearLayout selectedGiftLayout = view.findViewById(R.id.selectedGiftLayout);
android.widget.ImageView selectedGiftIcon = view.findViewById(R.id.selectedGiftIcon);
android.widget.TextView selectedGiftName = view.findViewById(R.id.selectedGiftName);
android.widget.TextView selectedGiftPrice = view.findViewById(R.id.selectedGiftPrice);
// 数量控制
android.widget.TextView giftCountText = view.findViewById(R.id.giftCount);
android.widget.ImageButton decreaseButton = view.findViewById(R.id.decreaseButton);
android.widget.ImageButton increaseButton = view.findViewById(R.id.increaseButton);
final int[] giftCount = {1}; // 使用数组以便在lambda中修改
// 礼物选择监听
giftAdapter.setOnGiftClickListener(gift -> {
selectedGiftLayout.setVisibility(View.VISIBLE);
selectedGiftIcon.setImageResource(gift.getIconResId());
selectedGiftName.setText(gift.getName());
selectedGiftPrice.setText(gift.getFormattedPrice());
giftCount[0] = 1;
giftCountText.setText(String.valueOf(giftCount[0]));
});
// 减少数量
decreaseButton.setOnClickListener(v -> {
if (giftCount[0] > 1) {
giftCount[0]--;
giftCountText.setText(String.valueOf(giftCount[0]));
}
});
// 增加数量
increaseButton.setOnClickListener(v -> {
if (giftCount[0] < 99) {
giftCount[0]++;
giftCountText.setText(String.valueOf(giftCount[0]));
}
});
// 赠送按钮
com.google.android.material.button.MaterialButton sendGiftButton = view.findViewById(R.id.sendGiftButton);
sendGiftButton.setOnClickListener(v -> {
Gift selectedGift = giftAdapter.getSelectedGift();
if (selectedGift == null) {
Toast.makeText(this, "请选择礼物", Toast.LENGTH_SHORT).show();
return;
}
int totalPrice = selectedGift.getPrice() * giftCount[0];
if (totalPrice > userCoinBalance) {
Toast.makeText(this, "金币余额不足,请充值", Toast.LENGTH_SHORT).show();
// 显示充值对话框
showRechargeDialog();
return;
}
// 调用后端接口赠送礼物
sendGiftToBackend(selectedGift, giftCount[0], totalPrice, coinBalance, selectedGiftLayout, giftCountText);
});
// 关闭按钮
view.findViewById(R.id.closeButton).setOnClickListener(v -> giftDialog.dismiss());
// 充值按钮
view.findViewById(R.id.rechargeButton).setOnClickListener(v -> showRechargeDialog());
}
giftDialog.show();
}
/**
* 显示充值对话框
*/
private void showRechargeDialog() {
// 跳转到钱包页面进行充值
Intent intent = new Intent(this, WalletActivity.class);
startActivity(intent);
}
/**
* 加载充值选项列表
*/
private void loadRechargeOptions(RechargeAdapter adapter, View dialogView) {
ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<List<RechargeOptionResponse>>> call = apiService.getRechargeOptions();
call.enqueue(new Callback<ApiResponse<List<RechargeOptionResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<List<RechargeOptionResponse>>> call,
Response<ApiResponse<List<RechargeOptionResponse>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<List<RechargeOptionResponse>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
List<RechargeOption> options = new ArrayList<>();
for (RechargeOptionResponse optionResponse : apiResponse.getData()) {
RechargeOption option = new RechargeOption(
optionResponse.getId(),
optionResponse.getCoinAmount().intValue(),
optionResponse.getPrice().doubleValue(),
optionResponse.getDiscountLabel()
);
options.add(option);
}
adapter.setOptions(options);
} else {
Toast.makeText(RoomDetailActivity.this,
"加载充值选项失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
// 使用默认选项
setDefaultRechargeOptions(adapter);
}
} else {
Toast.makeText(RoomDetailActivity.this,
"加载充值选项失败",
Toast.LENGTH_SHORT).show();
// 使用默认选项
setDefaultRechargeOptions(adapter);
}
}
@Override
public void onFailure(Call<ApiResponse<List<RechargeOptionResponse>>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
// 使用默认选项
setDefaultRechargeOptions(adapter);
}
});
}
/**
* 设置默认充值选项(当接口失败时使用)
*/
private void setDefaultRechargeOptions(RechargeAdapter adapter) {
List<RechargeOption> rechargeOptions = new ArrayList<>();
rechargeOptions.add(new RechargeOption("1", 100, 10.0, "首充优惠"));
rechargeOptions.add(new RechargeOption("2", 300, 30.0));
rechargeOptions.add(new RechargeOption("3", 500, 50.0, "热门"));
rechargeOptions.add(new RechargeOption("4", 1000, 100.0));
rechargeOptions.add(new RechargeOption("5", 3000, 300.0, "最划算"));
rechargeOptions.add(new RechargeOption("6", 5000, 500.0));
adapter.setOptions(rechargeOptions);
}
/**
* 创建充值订单
*/
private void createRechargeOrder(RechargeOption selectedOption, androidx.appcompat.app.AlertDialog rechargeDialog) {
ApiService apiService = ApiClient.getService(this);
CreateRechargeRequest request = new CreateRechargeRequest(
Integer.parseInt(selectedOption.getId()),
new java.math.BigDecimal(selectedOption.getCoinAmount()),
new java.math.BigDecimal(selectedOption.getPrice())
);
Call<ApiResponse<CreateRechargeResponse>> call = apiService.createRecharge(request);
call.enqueue(new Callback<ApiResponse<CreateRechargeResponse>>() {
@Override
public void onResponse(Call<ApiResponse<CreateRechargeResponse>> call,
Response<ApiResponse<CreateRechargeResponse>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<CreateRechargeResponse> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
CreateRechargeResponse rechargeResponse = apiResponse.getData();
String orderId = rechargeResponse.getOrderId();
String paymentUrl = rechargeResponse.getPaymentUrl();
// 显示支付选择对话框
showPaymentMethodDialog(orderId, selectedOption, rechargeDialog);
} else {
Toast.makeText(RoomDetailActivity.this,
"创建充值订单失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(RoomDetailActivity.this,
"创建充值订单失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<CreateRechargeResponse>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* 显示支付方式选择对话框
*/
private void showPaymentMethodDialog(String orderId, RechargeOption selectedOption,
androidx.appcompat.app.AlertDialog rechargeDialog) {
String[] paymentMethods = {"支付宝支付", "微信支付"};
new MaterialAlertDialogBuilder(this)
.setTitle("选择支付方式")
.setItems(paymentMethods, (dialog, which) -> {
String payType;
String payChannel;
switch (which) {
case 0: // 支付宝
payType = "alipay";
payChannel = "appAliPay";
break;
case 1: // 微信
payType = "weixin";
payChannel = "weixinAppAndroid";
break;
default:
return;
}
// 调用支付接口
processPayment(orderId, payType, payChannel, selectedOption, rechargeDialog);
})
.setNegativeButton("取消", null)
.show();
}
/**
* 处理支付
*/
private void processPayment(String orderId, String payType, String payChannel,
RechargeOption selectedOption, androidx.appcompat.app.AlertDialog rechargeDialog) {
ApiService apiService = ApiClient.getService(this);
OrderPayRequest payRequest = new OrderPayRequest(orderId, payType, payChannel);
Call<ApiResponse<OrderPayResultResponse>> call = apiService.payment(payRequest);
call.enqueue(new Callback<ApiResponse<OrderPayResultResponse>>() {
@Override
public void onResponse(Call<ApiResponse<OrderPayResultResponse>> call,
Response<ApiResponse<OrderPayResultResponse>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<OrderPayResultResponse> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
OrderPayResultResponse payResult = apiResponse.getData();
// TODO: 集成支付SDK微信支付、支付宝等
// 1. 根据payType调用对应的支付SDK
// 2. 传入jsConfig参数发起支付
// 3. 监听支付结果回调
// 4. 支付成功后查询订单状态并更新余额
Toast.makeText(RoomDetailActivity.this,
"支付接口调用成功,订单号: " + payResult.getOrderNo() +
"\n请集成支付SDK完成实际支付",
Toast.LENGTH_LONG).show();
// TODO: 集成支付SDK后在支付成功回调中更新余额
// 支付成功后应该调用后端接口查询订单状态并更新余额
rechargeDialog.dismiss();
} else {
Toast.makeText(RoomDetailActivity.this,
"支付失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(RoomDetailActivity.this,
"支付失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<OrderPayResultResponse>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* 从后端加载用户金币余额
*/
private void loadUserBalance(android.widget.TextView coinBalanceView) {
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<UserBalanceResponse>> call = apiService.getUserBalance();
call.enqueue(new Callback<ApiResponse<UserBalanceResponse>>() {
@Override
public void onResponse(Call<ApiResponse<UserBalanceResponse>> call,
Response<ApiResponse<UserBalanceResponse>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<UserBalanceResponse> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
userCoinBalance = apiResponse.getData().getCoinBalance().intValue();
coinBalanceView.setText(String.valueOf(userCoinBalance));
} else {
coinBalanceView.setText(String.valueOf(userCoinBalance));
}
} else {
coinBalanceView.setText(String.valueOf(userCoinBalance));
}
}
@Override
public void onFailure(Call<ApiResponse<UserBalanceResponse>> call, Throwable t) {
coinBalanceView.setText(String.valueOf(userCoinBalance));
}
});
}
/**
* 发送礼物到后端
*/
private void sendGiftToBackend(Gift selectedGift, int count, int totalPrice,
android.widget.TextView coinBalance,
android.widget.LinearLayout selectedGiftLayout,
android.widget.TextView giftCountText) {
if (room == null || TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
// 获取主播ID使用房间ID作为主播ID或者从房间信息中获取
Integer streamerId = Integer.parseInt(roomId);
SendGiftRequest request = new SendGiftRequest(
Integer.parseInt(selectedGift.getId()),
streamerId,
count
);
Call<ApiResponse<SendGiftResponse>> call = apiService.sendRoomGift(roomId, request);
call.enqueue(new Callback<ApiResponse<SendGiftResponse>>() {
@Override
public void onResponse(Call<ApiResponse<SendGiftResponse>> call,
Response<ApiResponse<SendGiftResponse>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<SendGiftResponse> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
SendGiftResponse giftResponse = apiResponse.getData();
// 更新余额
userCoinBalance = giftResponse.getNewBalance().intValue();
coinBalance.setText(String.valueOf(userCoinBalance));
// 在聊天区显示赠送消息
String giftMessage = String.format("送出了 %d 个 %s", count, selectedGift.getName());
addChatMessage(new ChatMessage("", giftMessage, true));
Toast.makeText(RoomDetailActivity.this, "赠送成功!", Toast.LENGTH_SHORT).show();
if (giftDialog != null) {
giftDialog.dismiss();
}
// 重置选择
if (giftAdapter != null) {
giftAdapter.setSelectedGift(null);
}
selectedGiftLayout.setVisibility(View.GONE);
giftCountText.setText("1");
} else {
Toast.makeText(RoomDetailActivity.this,
"赠送失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(RoomDetailActivity.this,
"赠送失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<SendGiftResponse>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* 加载房间信息
*/
private void loadRoomInfo() {
if (TextUtils.isEmpty(roomId)) {
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<Room>> call = apiService.getRoom(roomId);
call.enqueue(new Callback<ApiResponse<Room>>() {
@Override
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Room> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
room = apiResponse.getData();
android.util.Log.d("RoomDetail", "房间信息加载成功: streamerId=" + room.getStreamerId() +
", isFollowing=" + room.getIsFollowing());
// 更新关注按钮状态
updateFollowButtonState();
} else {
android.util.Log.w("RoomDetail", "加载房间信息失败: " + apiResponse.getMessage());
}
} else {
android.util.Log.e("RoomDetail", "加载房间信息失败: response code=" + response.code());
}
}
@Override
public void onFailure(Call<ApiResponse<Room>> call, Throwable t) {
android.util.Log.e("RoomDetail", "加载房间信息网络错误", t);
}
});
}
/**
* 更新关注按钮状态
*/
private void updateFollowButtonState() {
if (binding == null || binding.followButton == null || room == null) {
return;
}
Boolean isFollowing = room.getIsFollowing();
if (isFollowing != null && isFollowing) {
binding.followButton.setText("已关注");
binding.followButton.setEnabled(false);
} else {
binding.followButton.setText("关注");
binding.followButton.setEnabled(true);
}
}
/**
* 关注主播
*/
private void followStreamerBackend() {
if (room == null) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 获取主播用户ID
Integer streamerId = room.getStreamerId();
if (streamerId == null) {
Toast.makeText(this, "无法获取主播信息", Toast.LENGTH_SHORT).show();
android.util.Log.e("RoomDetail", "streamerId is null, room: " + room.getId());
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
// 使用正确的关注接口
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("userId", streamerId);
android.util.Log.d("RoomDetail", "关注主播: streamerId=" + streamerId + ", roomId=" + roomId);
Call<ApiResponse<Map<String, Object>>> call = apiService.followUser(body);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
android.util.Log.d("RoomDetail", "关注响应: code=" + apiResponse.getCode() +
", message=" + apiResponse.getMessage() +
", data=" + apiResponse.getData());
if (apiResponse.getCode() == 200) {
Toast.makeText(RoomDetailActivity.this, "已关注主播", Toast.LENGTH_SHORT).show();
// 更新按钮状态
if (binding != null && binding.followButton != null) {
binding.followButton.setText("已关注");
binding.followButton.setEnabled(false);
}
// 更新room对象的关注状态
if (room != null) {
room.setIsFollowing(true);
}
// 关注成功后,检查是否需要显示粉丝团横幅
checkAndShowFanGroupBanner();
} else {
Toast.makeText(RoomDetailActivity.this,
"关注失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
android.util.Log.e("RoomDetail", "关注失败: response not successful, code=" + response.code());
Toast.makeText(RoomDetailActivity.this,
"关注失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
android.util.Log.e("RoomDetail", "关注网络错误", t);
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* 开始直播
* 接口: POST /api/front/live/room/{id}/start
*/
private void startLiveStream() {
if (room == null || TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 检查登录状态和权限
if (!AuthHelper.requireLoginWithToast(this, "开始直播需要登录")) {
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<Map<String, Object>>> call = apiService.startLiveRoom(roomId);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200) {
Toast.makeText(RoomDetailActivity.this, "直播已开始", Toast.LENGTH_SHORT).show();
// 刷新房间信息
fetchRoom();
} else {
Toast.makeText(RoomDetailActivity.this,
"开始直播失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(RoomDetailActivity.this,
"开始直播失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* 结束直播
* 接口: POST /api/front/live/room/{id}/stop
*/
private void stopLiveStream() {
if (room == null || TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 检查登录状态和权限
if (!AuthHelper.requireLoginWithToast(this, "结束直播需要登录")) {
return;
}
// 显示确认对话框
new MaterialAlertDialogBuilder(this)
.setTitle("结束直播")
.setMessage("确定要结束直播吗?")
.setPositiveButton("确定", (dialog, which) -> {
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<Map<String, Object>>> call = apiService.stopLiveRoom(roomId);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200) {
Toast.makeText(RoomDetailActivity.this, "直播已结束", Toast.LENGTH_SHORT).show();
// 刷新房间信息
fetchRoom();
} else {
Toast.makeText(RoomDetailActivity.this,
"结束直播失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(RoomDetailActivity.this,
"结束直播失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
})
.setNegativeButton("取消", null)
.show();
}
/**
* 手动广播在线人数
* 接口: POST /api/live/online/broadcast/{roomId}
*/
private void broadcastOnlineCount() {
if (TextUtils.isEmpty(roomId)) {
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<Map<String, Object>>> call = apiService.broadcastOnlineCount(roomId);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200) {
android.util.Log.d("RoomDetail", "在线人数广播成功");
}
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
android.util.Log.e("RoomDetail", "在线人数广播失败: " + t.getMessage());
}
});
}
/**
* 显示礼物动画效果
* @param giftName 礼物名称
* @param count 礼物数量
* @param senderNickname 赠送者昵称
*/
private void showGiftAnimation(String giftName, int count, String senderNickname) {
// TODO: 实现礼物动画效果
// 这里可以使用Lottie动画库或自定义动画
// 示例显示一个Toast提示
runOnUiThread(() -> {
String message = senderNickname + " 送出了 " + count + "" + giftName;
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
// 可以在这里添加更复杂的动画效果,例如:
// 1. 使用Lottie播放礼物动画
// 2. 显示礼物图标飞行动画
// 3. 播放音效
// 4. 显示特效粒子
android.util.Log.d("GiftAnimation", "显示礼物动画: " + message);
});
}
// ==================== 粉丝团横幅功能 ====================
private Room currentRoom; // 当前房间信息(用于粉丝团功能)
/**
* 检查并显示粉丝团横幅
* 在关注成功后调用
*/
private void checkAndShowFanGroupBanner() {
if (room == null || room.getStreamerId() == null) {
android.util.Log.d("FanGroup", "无法获取主播信息,跳过粉丝团检查");
return;
}
android.util.Log.d("FanGroup", "检查主播粉丝团: streamerId=" + room.getStreamerId());
// 获取主播的粉丝团信息
ApiClient.getService(this).getStreamerFanGroup(room.getStreamerId())
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
Map<String, Object> fanGroup = response.body().getData();
if (fanGroup != null && !fanGroup.isEmpty() && fanGroup.containsKey("id")) {
// 检查是否已加入粉丝团
int fanGroupId = ((Number) fanGroup.get("id")).intValue();
android.util.Log.d("FanGroup", "找到粉丝团: id=" + fanGroupId);
checkFanGroupJoinStatus(fanGroupId, fanGroup);
} else {
android.util.Log.d("FanGroup", "主播没有创建粉丝团");
}
} else {
android.util.Log.d("FanGroup", "获取粉丝团信息失败或为空");
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
android.util.Log.e("FanGroup", "获取粉丝团信息失败", t);
}
});
}
/**
* 检查粉丝团加入状态
*/
private void checkFanGroupJoinStatus(int fanGroupId, Map<String, Object> fanGroup) {
ApiClient.getService(this).checkFanGroupJoined(fanGroupId)
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
Map<String, Object> joinStatus = response.body().getData();
Boolean joined = (Boolean) joinStatus.get("joined");
if (joined == null || !joined) {
// 未加入,显示横幅
android.util.Log.d("FanGroup", "用户未加入粉丝团,显示横幅");
showFanGroupBanner(fanGroupId, fanGroup);
} else {
android.util.Log.d("FanGroup", "用户已加入粉丝团");
}
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
android.util.Log.e("FanGroup", "检查粉丝团状态失败", t);
}
});
}
/**
* 显示粉丝团横幅
*/
private void showFanGroupBanner(int fanGroupId, Map<String, Object> fanGroup) {
runOnUiThread(() -> {
View bannerInclude = findViewById(R.id.fanGroupBannerInclude);
if (bannerInclude != null) {
bannerInclude.setVisibility(View.VISIBLE);
bannerInclude.setAlpha(0f);
bannerInclude.animate().alpha(1f).setDuration(300).start();
// 设置粉丝团名称
TextView tvFanGroupName = bannerInclude.findViewById(R.id.tvFanGroupName);
String fanGroupName = (String) fanGroup.get("name");
if (fanGroupName != null && tvFanGroupName != null) {
tvFanGroupName.setText("加入 " + fanGroupName);
}
// 加入按钮点击事件
com.google.android.material.button.MaterialButton btnJoin = bannerInclude.findViewById(R.id.btnJoinFanGroup);
if (btnJoin != null) {
btnJoin.setOnClickListener(v -> joinFanGroup(fanGroupId, bannerInclude));
}
// 关闭按钮点击事件
ImageButton btnClose = bannerInclude.findViewById(R.id.btnCloseBanner);
if (btnClose != null) {
btnClose.setOnClickListener(v -> hideFanGroupBanner(bannerInclude));
}
// 5秒后自动隐藏
handler.postDelayed(() -> {
if (bannerInclude.getVisibility() == View.VISIBLE) {
hideFanGroupBanner(bannerInclude);
}
}, 5000);
}
});
}
/**
* 加入粉丝团
*/
private void joinFanGroup(int fanGroupId, View bannerView) {
ApiClient.getService(this).joinFanGroup(fanGroupId)
.enqueue(new Callback<ApiResponse<String>>() {
@Override
public void onResponse(Call<ApiResponse<String>> call,
Response<ApiResponse<String>> response) {
runOnUiThread(() -> {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
Toast.makeText(RoomDetailActivity.this, "加入粉丝团成功!", Toast.LENGTH_SHORT).show();
hideFanGroupBanner(bannerView);
} else {
String msg = response.body() != null ? response.body().getMessage() : "加入失败";
Toast.makeText(RoomDetailActivity.this, msg, Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onFailure(Call<ApiResponse<String>> call, Throwable t) {
runOnUiThread(() -> {
Toast.makeText(RoomDetailActivity.this, "网络错误,请重试", Toast.LENGTH_SHORT).show();
});
}
});
}
/**
* 隐藏粉丝团横幅
*/
private void hideFanGroupBanner(View bannerView) {
if (bannerView != null) {
bannerView.animate()
.alpha(0f)
.setDuration(300)
.withEndAction(() -> bannerView.setVisibility(View.GONE))
.start();
}
}
}