1842 lines
73 KiB
Java
1842 lines
73 KiB
Java
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());
|
||
}
|
||
});
|
||
}
|
||
|
||
}
|