zhibo/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java

1842 lines
73 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.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.TextureView;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
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.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.CreateRechargeRequest;
import com.example.livestreaming.net.CreateRechargeResponse;
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.StreamConfig;
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;
import java.util.ArrayList;
import java.util.List;
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 {
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 Runnable chatSimulationRunnable;
private String roomId;
private Room room;
private ExoPlayer player;
private boolean triedAltUrl;
private IjkMediaPlayer ijkPlayer;
private Surface ijkSurface;
private String ijkUrl;
private String ijkFallbackHlsUrl;
private boolean ijkFallbackTried;
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;
private static final String WS_CHAT_BASE_URL = "ws://192.168.1.164:8081/ws/live/chat/";
// WebSocket - 在线人数
private WebSocket onlineCountWebSocket;
private OkHttpClient onlineCountWsClient;
private static final String WS_ONLINE_BASE_URL = "ws://192.168.1.164:8081/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 = 1000; // 用户金币余额(模拟数据)
// 模拟用户名列表
private final String[] simulatedUsers = {
"游戏达人", "直播观众", "路过的小伙伴", "老铁666", "主播加油",
"夜猫子", "学生党", "上班族", "游戏爱好者", "直播粉丝"
};
// 模拟弹幕内容
private final String[] simulatedMessages = {
"主播666", "这个操作厉害了", "学到了学到了", "主播加油!",
"太强了", "这波操作可以", "牛牛牛", "厉害厉害", "支持主播",
"精彩精彩", "继续继续", "好看好看", "赞赞赞", "棒棒棒"
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 隐藏ActionBar使用自定义顶部栏
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
binding = ActivityRoomDetailNewBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ApiClient.getService(getApplicationContext());
roomId = getIntent().getStringExtra(EXTRA_ROOM_ID);
triedAltUrl = false;
setupUI();
setupChat();
setupGifts();
// TODO: 接入后端接口 - 记录观看历史
// 接口路径: POST /api/watch/history
// 请求参数:
// - userId: 当前用户ID从token中获取
// - roomId: 房间ID
// - watchTime: 观看时间(时间戳,可选)
// 返回数据格式: ApiResponse<{success: boolean}>
// 进入房间时调用,用于记录用户观看历史
// 添加欢迎消息
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
}
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());
}
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());
// 输入框回车发送
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 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(WS_CHAT_BASE_URL + 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 ("connected".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 clientId = AuthStore.getUserId(this) > 0 ?
String.valueOf(AuthStore.getUserId(this)) :
"guest_" + System.currentTimeMillis();
String wsUrl = WS_ONLINE_BASE_URL + 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);
if (getSupportActionBar() != null) {
getSupportActionBar().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);
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
isFullscreen = true;
if (binding != null) binding.exitFullscreenButton.setVisibility(View.GONE);
}
}
@Override
protected void onStart() {
super.onStart();
startPolling();
connectWebSocket(); // 连接 WebSocket
}
@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 startChatSimulation() {
// TODO: 接入后端接口 - 初始化时获取历史弹幕消息
// 接口路径: GET /api/rooms/{roomId}/messages
// 请求参数:
// - roomId: 房间ID路径参数
// - limit (可选): 获取最近N条消息默认50
// 返回数据格式: ApiResponse<List<ChatMessage>>
// 进入直播间时先获取最近50条历史消息显示在聊天列表中
stopChatSimulation();
chatSimulationRunnable = () -> {
if (isFinishing() || isDestroyed()) return;
// TODO: 这里应该改为从WebSocket或轮询接口获取新消息而不是模拟生成
// 随机生成弹幕,降低概率
if (random.nextFloat() < 0.25f) { // 25%概率生成弹幕
String user = simulatedUsers[random.nextInt(simulatedUsers.length)];
String message = simulatedMessages[random.nextInt(simulatedMessages.length)];
addChatMessage(new ChatMessage(user, message));
}
// 随机间隔5-12秒减少频率
int delay = 5000 + random.nextInt(7000);
handler.postDelayed(chatSimulationRunnable, delay);
};
// 首次延迟3秒开始
handler.postDelayed(chatSimulationRunnable, 3000);
}
private void stopChatSimulation() {
if (chatSimulationRunnable != null) {
handler.removeCallbacks(chatSimulationRunnable);
chatSimulationRunnable = null;
}
}
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.loading.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.loading.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.loading.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) {
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);
// TODO: 接入后端接口 - 获取实时观看人数
// 接口路径: GET /api/rooms/{roomId}/viewers/count
// 请求参数: roomId (路径参数)
// 返回数据格式: ApiResponse<{viewerCount: number}>
// 建议使用WebSocket实时推送观看人数变化或每10-15秒轮询一次
// 设置观看人数(模拟)
int viewerCount = r.getViewerCount() > 0 ? r.getViewerCount() :
100 + random.nextInt(500);
binding.topViewerCount.setText(String.valueOf(viewerCount));
} else {
binding.liveTag.setVisibility(View.GONE);
binding.offlineLayout.setVisibility(View.VISIBLE);
releasePlayer();
return;
}
// 获取播放地址
String playUrl = null;
String fallbackHlsUrl = null;
if (r.getStreamUrls() != null) {
// 优先使用HTTP-FLV延迟更低
playUrl = r.getStreamUrls().getFlv();
fallbackHlsUrl = r.getStreamUrls().getHls();
if (TextUtils.isEmpty(playUrl)) playUrl = fallbackHlsUrl;
}
if (!TextUtils.isEmpty(playUrl)) {
ensurePlayer(playUrl, fallbackHlsUrl);
} else {
// 没有播放地址时显示离线状态
binding.offlineLayout.setVisibility(View.VISIBLE);
releasePlayer();
}
}
private void ensurePlayer(String url, String fallbackHlsUrl) {
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);
}
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;
ExoPlayer exo = new ExoPlayer.Builder(this)
.setLoadControl(new androidx.media3.exoplayer.DefaultLoadControl.Builder()
.setBufferDurationsMs(
1000,
3000,
500,
1000
)
.build())
.build();
binding.playerView.setPlayer(exo);
String computedAltUrl = altUrl;
if (TextUtils.isEmpty(computedAltUrl)) computedAltUrl = getAltHlsUrl(url);
String finalComputedAltUrl = computedAltUrl;
exo.addListener(new Player.Listener() {
@Override
public void onPlayerError(PlaybackException error) {
if (triedAltUrl || TextUtils.isEmpty(finalComputedAltUrl)) {
binding.offlineLayout.setVisibility(View.VISIBLE);
return;
}
triedAltUrl = true;
exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl));
exo.prepare();
exo.setPlayWhenReady(true);
}
@Override
public void onPlaybackStateChanged(int playbackState) {
if (playbackState == Player.STATE_READY) {
binding.offlineLayout.setVisibility(View.GONE);
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
}
}
});
exo.setMediaItem(MediaItem.fromUri(url));
exo.prepare();
exo.setPlayWhenReady(true);
player = exo;
}
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
ensureIjkLibsLoaded();
releaseExoPlayer();
releaseIjkPlayer();
ijkUrl = flvUrl;
ijkFallbackHlsUrl = fallbackHlsUrl;
ijkFallbackTried = false;
if (binding != null) {
binding.playerView.setVisibility(View.GONE);
binding.flvTextureView.setVisibility(View.VISIBLE);
}
TextureView view = binding.flvTextureView;
TextureView.SurfaceTextureListener listener = new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(@NonNull android.graphics.SurfaceTexture surfaceTexture, int width, int height) {
ijkSurface = new Surface(surfaceTexture);
prepareIjk(flvUrl);
}
@Override
public void onSurfaceTextureSizeChanged(@NonNull android.graphics.SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull android.graphics.SurfaceTexture surface) {
releaseIjkPlayer();
return true;
}
@Override
public void onSurfaceTextureUpdated(@NonNull android.graphics.SurfaceTexture surface) {
}
};
view.setSurfaceTextureListener(listener);
if (view.isAvailable() && view.getSurfaceTexture() != null) {
ijkSurface = new Surface(view.getSurfaceTexture());
prepareIjk(flvUrl);
}
}
private void prepareIjk(String url) {
if (ijkSurface == null) return;
IjkMediaPlayer p = new IjkMediaPlayer();
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
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", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 300);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
p.setOnPreparedListener(mp -> {
binding.offlineLayout.setVisibility(View.GONE);
mp.start();
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
});
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) {
binding.offlineLayout.setVisibility(View.VISIBLE);
return true;
}
ijkFallbackTried = true;
startHls(ijkFallbackHlsUrl, null);
return true;
});
ijkPlayer = p;
try {
p.setSurface(ijkSurface);
p.setDataSource(url);
p.prepareAsync();
} catch (Exception e) {
if (!TextUtils.isEmpty(ijkFallbackHlsUrl)) {
startHls(ijkFallbackHlsUrl, null);
} else {
binding.offlineLayout.setVisibility(View.VISIBLE);
}
}
}
private static void ensureIjkLibsLoaded() {
if (ijkLibLoaded) return;
try {
IjkMediaPlayer.loadLibrariesOnce(null);
IjkMediaPlayer.native_profileBegin("libijkplayer.so");
} catch (Throwable ignored) {
}
ijkLibLoaded = true;
}
private void releasePlayer() {
releaseExoPlayer();
releaseIjkPlayer();
if (binding != null) {
binding.playerView.setPlayer(null);
binding.flvTextureView.setVisibility(View.GONE);
binding.playerView.setVisibility(View.VISIBLE);
}
}
private void releaseExoPlayer() {
if (player != null) {
player.release();
player = null;
}
}
private void releaseIjkPlayer() {
if (ijkPlayer != null) {
ijkPlayer.reset();
ijkPlayer.release();
ijkPlayer = null;
}
if (ijkSurface != null) {
ijkSurface.release();
ijkSurface = 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 onDestroy() {
super.onDestroy();
// 确保Handler回调被清理防止内存泄漏
if (handler != null) {
if (pollRunnable != null) {
handler.removeCallbacks(pollRunnable);
}
if (chatSimulationRunnable != null) {
handler.removeCallbacks(chatSimulationRunnable);
}
// 清理弹幕心跳和重连回调
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.getMsg());
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<>();
availableGifts.add(new Gift("1", "玫瑰", 10, R.drawable.ic_gift_rose, 1));
availableGifts.add(new Gift("2", "爱心", 20, R.drawable.ic_gift_heart, 1));
availableGifts.add(new Gift("3", "蛋糕", 50, R.drawable.ic_gift_cake, 2));
availableGifts.add(new Gift("4", "星星", 100, R.drawable.ic_gift_star, 2));
availableGifts.add(new Gift("5", "钻石", 200, R.drawable.ic_gift_diamond, 3));
availableGifts.add(new Gift("6", "皇冠", 500, R.drawable.ic_gift_crown, 4));
availableGifts.add(new Gift("7", "跑车", 1000, R.drawable.ic_gift_car, 5));
availableGifts.add(new Gift("8", "火箭", 2000, R.drawable.ic_gift_rocket, 5));
}
/**
* 显示礼物选择弹窗
*/
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() {
// 创建充值对话框
View dialogView = getLayoutInflater().inflate(R.layout.dialog_recharge, null);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setView(dialogView);
androidx.appcompat.app.AlertDialog rechargeDialog = builder.create();
// 设置对话框背景透明,使用自定义圆角
if (rechargeDialog.getWindow() != null) {
rechargeDialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
}
// 初始化充值选项列表
androidx.recyclerview.widget.RecyclerView recyclerView = dialogView.findViewById(R.id.rechargeOptionsRecyclerView);
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
RechargeAdapter rechargeAdapter = new RechargeAdapter();
recyclerView.setAdapter(rechargeAdapter);
// 显示当前余额
android.widget.TextView currentBalance = dialogView.findViewById(R.id.currentBalance);
currentBalance.setText(String.valueOf(userCoinBalance));
// 加载充值选项列表
loadRechargeOptions(rechargeAdapter, dialogView);
// 取消按钮
dialogView.findViewById(R.id.cancelButton).setOnClickListener(v -> rechargeDialog.dismiss());
// 确认充值按钮
dialogView.findViewById(R.id.confirmRechargeButton).setOnClickListener(v -> {
RechargeOption selectedOption = rechargeAdapter.getSelectedOption();
if (selectedOption == null) {
Toast.makeText(this, "请选择充值金额", Toast.LENGTH_SHORT).show();
return;
}
// 调用后端接口创建充值订单
createRechargeOrder(selectedOption, rechargeDialog);
});
rechargeDialog.show();
}
/**
* 加载充值选项列表
*/
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;
case 2: // 余额支付(模拟)
// 模拟充值成功
simulateRechargeSuccess(selectedOption, rechargeDialog);
return;
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.processPayment(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();
// 暂时模拟支付成功
simulateRechargeSuccess(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<OrderPayResultResponse>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* 模拟充值成功
*/
private void simulateRechargeSuccess(RechargeOption selectedOption,
androidx.appcompat.app.AlertDialog rechargeDialog) {
userCoinBalance += selectedOption.getCoinAmount();
// 更新礼物弹窗中的余额显示
if (giftDialog != null && giftDialog.isShowing()) {
View giftView = giftDialog.findViewById(R.id.coinBalance);
if (giftView instanceof android.widget.TextView) {
((android.widget.TextView) giftView).setText(String.valueOf(userCoinBalance));
}
}
Toast.makeText(this,
String.format("充值成功!获得 %d 金币", selectedOption.getCoinAmount()),
Toast.LENGTH_SHORT).show();
rechargeDialog.dismiss();
}
/**
* 从后端加载用户金币余额
*/
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());
SendGiftRequest request = new SendGiftRequest();
request.setRoomId(Integer.parseInt(roomId));
request.setGiftId(Integer.parseInt(selectedGift.getId()));
request.setCount(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.getMsg(),
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 followStreamerBackend() {
if (room == null) {
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("streamerId", room.getStreamerId());
body.put("action", "follow");
Call<ApiResponse<Map<String, Object>>> call = apiService.followStreamer(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();
if (apiResponse.getCode() == 200) {
Toast.makeText(RoomDetailActivity.this, "已关注主播", Toast.LENGTH_SHORT).show();
binding.followButton.setText("已关注");
binding.followButton.setEnabled(false);
} else {
Toast.makeText(RoomDetailActivity.this,
"关注失败: " + apiResponse.getMsg(),
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}/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.getMsg(),
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.getMsg(),
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());
}
});
}
}