diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml
index 07e63db3..3a54e02b 100644
--- a/android-app/app/src/main/AndroidManifest.xml
+++ b/android-app/app/src/main/AndroidManifest.xml
@@ -35,7 +35,9 @@
+ android:exported="false"
+ android:configChanges="orientation|screenSize|keyboardHidden"
+ android:screenOrientation="portrait" />
{
- fetchRooms();
- // 增加轮询间隔,减少网络请求频率
- handler.postDelayed(pollRunnable, 10000);
+ // 检查Activity是否还在前台
+ if (!isFinishing() && !isDestroyed()) {
+ fetchRooms();
+ // 增加轮询间隔,减少网络请求频率
+ handler.postDelayed(pollRunnable, 15000);
+ }
};
// 延迟首次请求,让界面先显示
- handler.postDelayed(pollRunnable, 1000);
+ handler.postDelayed(pollRunnable, 2000);
}
private void stopPolling() {
@@ -227,8 +231,8 @@ public class MainActivity extends AppCompatActivity {
dialog.dismiss();
fetchRooms();
- // 显示推流信息
- showStreamInfo(room);
+ // 显示优化的推流信息弹窗
+ showOptimizedStreamInfo(room);
// 可选:跳转到房间详情
// Intent intent = new Intent(MainActivity.this, RoomDetailActivity.class);
@@ -239,7 +243,19 @@ public class MainActivity extends AppCompatActivity {
@Override
public void onFailure(Call> call, Throwable t) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
- Toast.makeText(MainActivity.this, "网络错误:" + (t != null ? t.getMessage() : ""), Toast.LENGTH_SHORT).show();
+ String errorMsg = "网络错误";
+ if (t != null) {
+ if (t.getMessage() != null && t.getMessage().contains("Unable to resolve host")) {
+ errorMsg = "无法连接服务器,请检查网络";
+ } else if (t.getMessage() != null && t.getMessage().contains("timeout")) {
+ errorMsg = "连接超时,请检查服务器是否运行";
+ } else if (t.getMessage() != null && t.getMessage().contains("Connection refused")) {
+ errorMsg = "连接被拒绝,请确保后端服务已启动";
+ } else {
+ errorMsg = "网络错误:" + t.getMessage();
+ }
+ }
+ Toast.makeText(MainActivity.this, errorMsg, Toast.LENGTH_LONG).show();
}
});
});
@@ -248,44 +264,62 @@ public class MainActivity extends AppCompatActivity {
dialog.show();
}
- private void showStreamInfo(Room room) {
+ private void showOptimizedStreamInfo(Room room) {
String streamKey = room != null ? room.getStreamKey() : null;
String rtmp = room != null && room.getStreamUrls() != null ? room.getStreamUrls().getRtmp() : null;
- String rtmpForObs = null;
- if (!TextUtils.isEmpty(rtmp) && rtmp.contains("10.0.2.2")) {
+ // 将RTMP地址中的10.0.2.2或192.168.x.x替换为localhost供OBS使用
+ String rtmpForObs = rtmp;
+ if (!TextUtils.isEmpty(rtmp)) {
try {
Uri u = Uri.parse(rtmp);
int port = u.getPort();
String path = u.getPath();
if (port > 0 && !TextUtils.isEmpty(path)) {
+ // OBS推流时使用localhost
rtmpForObs = "rtmp://localhost:" + port + path;
}
} catch (Exception ignored) {
}
}
- String msg = "";
- if (!TextUtils.isEmpty(rtmp)) {
- msg += "推流地址(服务器):\n" + rtmp + "\n\n";
- }
- if (!TextUtils.isEmpty(rtmpForObs)) {
- msg += "电脑本机 OBS 可用(等价地址):\n" + rtmpForObs + "\n\n";
- }
- if (!TextUtils.isEmpty(streamKey)) {
- msg += "推流密钥(Stream Key):\n" + streamKey + "\n\n";
- }
- msg += "提示:用 OBS 推流时,服务器填上面的推流地址,密钥填 streamKey。";
+ // 创建自定义弹窗布局
+ View dialogView = getLayoutInflater().inflate(R.layout.dialog_stream_info, null);
+
+ // 找到视图组件
+ TextView addressText = dialogView.findViewById(R.id.addressText);
+ TextView keyText = dialogView.findViewById(R.id.keyText);
+
+ // 设置文本内容
+ String displayAddress = !TextUtils.isEmpty(rtmpForObs) ? rtmpForObs : rtmp;
+ addressText.setText(displayAddress != null ? displayAddress : "");
+ keyText.setText(streamKey != null ? streamKey : "");
- String copyRtmp = rtmp;
- String copyKey = streamKey;
+ AlertDialog dialog = new AlertDialog.Builder(this)
+ .setTitle("🎉 直播间创建成功")
+ .setView(dialogView)
+ .setPositiveButton("开始直播", (d, w) -> {
+ d.dismiss();
+ // 跳转到直播播放页面
+ Intent intent = new Intent(MainActivity.this, RoomDetailActivity.class);
+ intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId());
+ startActivity(intent);
+ })
+ .setNegativeButton("稍后开始", (d, w) -> d.dismiss())
+ .create();
- new AlertDialog.Builder(this)
- .setTitle("已创建直播间")
- .setMessage(msg)
- .setNegativeButton("复制地址", (d, w) -> copyToClipboard("rtmp", copyRtmp))
- .setPositiveButton("复制密钥", (d, w) -> copyToClipboard("streamKey", copyKey))
- .show();
+ dialog.show();
+
+ // 设置复制按钮点击事件
+ dialogView.findViewById(R.id.copyAddressBtn).setOnClickListener(v -> {
+ copyToClipboard("推流地址", displayAddress);
+ Toast.makeText(this, "推流地址已复制", Toast.LENGTH_SHORT).show();
+ });
+
+ dialogView.findViewById(R.id.copyKeyBtn).setOnClickListener(v -> {
+ copyToClipboard("推流密钥", streamKey);
+ Toast.makeText(this, "推流密钥已复制", Toast.LENGTH_SHORT).show();
+ });
}
private void copyToClipboard(String label, String text) {
@@ -300,9 +334,16 @@ public class MainActivity extends AppCompatActivity {
}
private void fetchRooms() {
+ // 避免重复请求
+ if (isFetching) return;
+
isFetching = true;
lastFetchMs = System.currentTimeMillis();
- binding.loading.setVisibility(View.VISIBLE);
+
+ // 只在没有数据时显示loading
+ if (adapter.getItemCount() == 0) {
+ binding.loading.setVisibility(View.VISIBLE);
+ }
ApiClient.getService().getRooms().enqueue(new Callback>>() {
@Override
diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
index 7f021880..650699df 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
@@ -1,29 +1,35 @@
package com.example.livestreaming;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.net.Uri;
+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.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.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.content.ContextCompat;
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 com.example.livestreaming.databinding.ActivityRoomDetailBinding;
+import com.example.livestreaming.databinding.ActivityRoomDetailNewBinding;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.Room;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@@ -32,60 +38,191 @@ public class RoomDetailActivity extends AppCompatActivity {
public static final String EXTRA_ROOM_ID = "extra_room_id";
- private ActivityRoomDetailBinding binding;
+ 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 boolean isFullscreen = false;
+
+ private ChatAdapter chatAdapter;
+ private List chatMessages = new ArrayList<>();
+ private Random random = new Random();
+
+ // 模拟用户名列表
+ private final String[] simulatedUsers = {
+ "游戏达人", "直播观众", "路过的小伙伴", "老铁666", "主播加油",
+ "夜猫子", "学生党", "上班族", "游戏爱好者", "直播粉丝"
+ };
+
+ // 模拟弹幕内容
+ private final String[] simulatedMessages = {
+ "主播666", "这个操作厉害了", "学到了学到了", "主播加油!",
+ "太强了", "这波操作可以", "牛牛牛", "厉害厉害", "支持主播",
+ "精彩精彩", "继续继续", "好看好看", "赞赞赞", "棒棒棒"
+ };
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- binding = ActivityRoomDetailBinding.inflate(getLayoutInflater());
+
+ // 隐藏ActionBar,使用自定义顶部栏
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().hide();
+ }
+
+ binding = ActivityRoomDetailNewBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
roomId = getIntent().getStringExtra(EXTRA_ROOM_ID);
-
- binding.backButton.setOnClickListener(v -> finish());
- binding.copyRtmpButton.setOnClickListener(v -> {
- String alt = binding.rtmpAltValue.getText() != null ? binding.rtmpAltValue.getText().toString() : null;
- if (!TextUtils.isEmpty(alt) && binding.rtmpAltGroup.getVisibility() == View.VISIBLE) {
- copyToClipboard("rtmp", alt);
- return;
- }
- String raw = binding.rtmpValue.getText() != null ? binding.rtmpValue.getText().toString() : null;
- copyToClipboard("rtmp", raw);
- });
- binding.copyKeyButton.setOnClickListener(v -> copyToClipboard("streamKey", binding.keyValue.getText() != null ? binding.keyValue.getText().toString() : null));
- binding.deleteButton.setOnClickListener(v -> confirmDelete());
-
triedAltUrl = false;
+
+ setupUI();
+ setupChat();
+
+ // 添加欢迎消息
+ addChatMessage(new ChatMessage("欢迎来到直播间!", true));
+ }
+
+ private void setupUI() {
+ // 返回按钮
+ binding.backButton.setOnClickListener(v -> finish());
+
+ // 全屏按钮
+ binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen());
+
+ // 关注按钮
+ binding.followButton.setOnClickListener(v -> {
+ Toast.makeText(this, "已关注主播", Toast.LENGTH_SHORT).show();
+ binding.followButton.setText("已关注");
+ binding.followButton.setEnabled(false);
+ });
+ }
+
+ private void setupChat() {
+ // 设置弹幕适配器
+ chatAdapter = new ChatAdapter();
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this);
+ layoutManager.setStackFromEnd(true); // 从底部开始显示
+ binding.chatRecyclerView.setLayoutManager(layoutManager);
+ binding.chatRecyclerView.setAdapter(chatAdapter);
+
+ // 发送按钮点击事件
+ 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() {
+ String message = binding.chatInput.getText() != null ?
+ binding.chatInput.getText().toString().trim() : "";
+
+ if (!TextUtils.isEmpty(message)) {
+ addChatMessage(new ChatMessage("我", message));
+ binding.chatInput.setText("");
+
+ // 模拟其他用户回复
+ handler.postDelayed(() -> {
+ if (random.nextFloat() < 0.3f) { // 30%概率有人回复
+ String user = simulatedUsers[random.nextInt(simulatedUsers.length)];
+ String reply = simulatedMessages[random.nextInt(simulatedMessages.length)];
+ addChatMessage(new ChatMessage(user, reply));
+ }
+ }, 1000 + random.nextInt(3000));
+ }
+ }
+
+ 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);
+ getSupportActionBar().show();
+ isFullscreen = false;
+ } else {
+ // 进入全屏
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().hide();
+ }
+ isFullscreen = true;
+ }
}
@Override
protected void onStart() {
super.onStart();
startPolling();
+ startChatSimulation();
}
@Override
protected void onStop() {
super.onStop();
stopPolling();
+ stopChatSimulation();
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);
+ } else {
+ // 竖屏时显示所有UI元素
+ binding.topBar.setVisibility(View.VISIBLE);
+ binding.roomInfoLayout.setVisibility(View.VISIBLE);
+ binding.chatLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
private void startPolling() {
stopPolling();
+ // 首次立即获取房间信息
+ fetchRoom();
+
pollRunnable = () -> {
- fetchRoom();
- handler.postDelayed(pollRunnable, 5000);
+ if (!isFinishing() && !isDestroyed()) {
+ fetchRoom();
+ handler.postDelayed(pollRunnable, 15000); // 15秒轮询一次,减少压力
+ }
};
- handler.post(pollRunnable);
+ // 延迟15秒后开始轮询
+ handler.postDelayed(pollRunnable, 15000);
}
private void stopPolling() {
@@ -95,6 +232,36 @@ public class RoomDetailActivity extends AppCompatActivity {
}
}
+ private void startChatSimulation() {
+ stopChatSimulation();
+ chatSimulationRunnable = () -> {
+ if (isFinishing() || isDestroyed()) return;
+
+ // 随机生成弹幕,降低概率
+ 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() {
if (TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "房间不存在", Toast.LENGTH_SHORT).show();
@@ -102,18 +269,26 @@ public class RoomDetailActivity extends AppCompatActivity {
return;
}
- binding.loading.setVisibility(View.VISIBLE);
+ // 只在首次加载时显示loading
+ if (isFirstLoad) {
+ binding.loading.setVisibility(View.VISIBLE);
+ }
ApiClient.getService().getRoom(roomId).enqueue(new Callback>() {
@Override
public void onResponse(Call> call, Response> response) {
+ if (isFinishing() || isDestroyed()) return;
+
binding.loading.setVisibility(View.GONE);
+ isFirstLoad = false;
ApiResponse body = response.body();
Room data = body != null ? body.getData() : null;
if (!response.isSuccessful() || data == null) {
- Toast.makeText(RoomDetailActivity.this, "房间不存在", Toast.LENGTH_SHORT).show();
- finish();
+ if (isFirstLoad) {
+ Toast.makeText(RoomDetailActivity.this, "房间不存在", Toast.LENGTH_SHORT).show();
+ finish();
+ }
return;
}
@@ -123,69 +298,60 @@ public class RoomDetailActivity extends AppCompatActivity {
@Override
public void onFailure(Call> call, Throwable t) {
+ if (isFinishing() || isDestroyed()) return;
binding.loading.setVisibility(View.GONE);
+ isFirstLoad = false;
}
});
}
private void bindRoom(Room r) {
- setTitle(r.getTitle() != null ? r.getTitle() : "Room");
+ 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);
- binding.titleText.setText(r.getTitle() != null ? r.getTitle() : "(Untitled)");
- binding.streamerText.setText(r.getStreamerName() != null ? ("主播: " + r.getStreamerName()) : "");
-
- binding.liveBadge.setText(r.isLive() ? "直播中" : "未开播");
- int badgeColor = ContextCompat.getColor(this, r.isLive() ? R.color.live_red : android.R.color.darker_gray);
- binding.liveBadge.setBackgroundColor(badgeColor);
-
- String rtmp = r.getStreamUrls() != null ? r.getStreamUrls().getRtmp() : null;
- String key = r.getStreamKey();
-
- binding.rtmpValue.setText(rtmp != null ? rtmp : "");
- binding.keyValue.setText(key != null ? key : "");
-
- String altRtmp = getAltRtmpForObs(rtmp);
- binding.rtmpAltValue.setText(altRtmp != null ? altRtmp : "");
- binding.rtmpAltGroup.setVisibility(!TextUtils.isEmpty(altRtmp) ? View.VISIBLE : View.GONE);
-
- if (!r.isLive()) {
- binding.playerContainer.setVisibility(View.GONE);
- binding.offlineHint.setVisibility(View.VISIBLE);
+ // 设置直播状态
+ if (r.isLive()) {
+ binding.liveTag.setVisibility(View.VISIBLE);
+ binding.offlineLayout.setVisibility(View.GONE);
+
+ // 设置观看人数(模拟)
+ 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;
}
- binding.offlineHint.setVisibility(View.GONE);
- binding.playerContainer.setVisibility(View.VISIBLE);
-
+ // 获取播放地址
String playUrl = null;
if (r.getStreamUrls() != null) {
- playUrl = r.getStreamUrls().getHls();
- if (playUrl == null || playUrl.trim().isEmpty()) {
- playUrl = r.getStreamUrls().getFlv();
+ // 优先使用HTTP-FLV,延迟更低
+ playUrl = r.getStreamUrls().getFlv();
+ if (TextUtils.isEmpty(playUrl)) {
+ playUrl = r.getStreamUrls().getHls();
}
}
- if (TextUtils.isEmpty(playUrl)) {
+ if (!TextUtils.isEmpty(playUrl)) {
+ ensurePlayer(playUrl);
+ } else {
+ // 没有播放地址时显示离线状态
+ binding.offlineLayout.setVisibility(View.VISIBLE);
releasePlayer();
- return;
}
-
- if (r.getStreamUrls() != null) {
- String hls = r.getStreamUrls().getHls();
- String flv = r.getStreamUrls().getFlv();
- if (TextUtils.isEmpty(hls) && !TextUtils.isEmpty(flv)) {
- Toast.makeText(this, "提示:当前没有 HLS 地址,Android 可能无法播放 FLV。建议在后端启用 FFmpeg/HLS。", Toast.LENGTH_LONG).show();
- }
- }
-
- ensurePlayer(playUrl);
}
private void ensurePlayer(String url) {
- if (!TextUtils.isEmpty(url) && url.endsWith(".flv")) {
- Toast.makeText(this, "提示:Android 原生播放器可能无法播放 FLV,如黑屏请启用 HLS。", Toast.LENGTH_LONG).show();
- }
if (player != null) {
MediaItem current = player.getCurrentMediaItem();
String currentUri = current != null && current.localConfiguration != null && current.localConfiguration.uri != null
@@ -196,24 +362,49 @@ public class RoomDetailActivity extends AppCompatActivity {
}
releasePlayer();
-
triedAltUrl = false;
- ExoPlayer exo = new ExoPlayer.Builder(this).build();
+ // 创建低延迟播放器配置
+ ExoPlayer exo = new ExoPlayer.Builder(this)
+ .setLoadControl(new androidx.media3.exoplayer.DefaultLoadControl.Builder()
+ // 减少缓冲区大小,降低延迟
+ .setBufferDurationsMs(
+ 1000, // 最小缓冲时长 1秒
+ 3000, // 最大缓冲时长 3秒
+ 500, // 播放缓冲时长 0.5秒
+ 1000 // 播放后缓冲时长 1秒
+ )
+ .build())
+ .build();
+
binding.playerView.setPlayer(exo);
-
+
+ // 设置播放器监听器
String altUrl = getAltHlsUrl(url);
exo.addListener(new Player.Listener() {
@Override
public void onPlayerError(PlaybackException error) {
- if (triedAltUrl) return;
- if (TextUtils.isEmpty(altUrl)) return;
+ if (triedAltUrl || TextUtils.isEmpty(altUrl)) {
+ // 播放失败,显示离线状态
+ binding.offlineLayout.setVisibility(View.VISIBLE);
+ return;
+ }
triedAltUrl = true;
-
exo.setMediaItem(MediaItem.fromUri(altUrl));
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));
@@ -237,65 +428,5 @@ public class RoomDetailActivity extends AppCompatActivity {
return url.substring(0, url.length() - ".m3u8".length()) + "/index.m3u8";
}
- private String getAltRtmpForObs(String rtmp) {
- if (TextUtils.isEmpty(rtmp)) return null;
- if (!rtmp.contains("10.0.2.2")) return null;
- try {
- Uri u = Uri.parse(rtmp);
- int port = u.getPort();
- String path = u.getPath();
- if (port > 0 && !TextUtils.isEmpty(path)) {
- return "rtmp://localhost:" + port + path;
- }
- return null;
- } catch (Exception ignored) {
- return null;
- }
- }
-
- private void copyToClipboard(String label, String text) {
- if (TextUtils.isEmpty(text)) {
- Toast.makeText(this, "内容为空", Toast.LENGTH_SHORT).show();
- return;
- }
- ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
- if (cm == null) return;
- cm.setPrimaryClip(ClipData.newPlainText(label, text));
- Toast.makeText(this, "已复制", Toast.LENGTH_SHORT).show();
- }
-
- private void confirmDelete() {
- if (TextUtils.isEmpty(roomId)) return;
-
- new AlertDialog.Builder(this)
- .setTitle("删除房间")
- .setMessage("确定删除该直播间吗?")
- .setNegativeButton("取消", null)
- .setPositiveButton("删除", (d, w) -> deleteRoom())
- .show();
- }
-
- private void deleteRoom() {
- binding.loading.setVisibility(View.VISIBLE);
-
- ApiClient.getService().deleteRoom(roomId).enqueue(new Callback>() {
- @Override
- public void onResponse(Call> call, Response> response) {
- binding.loading.setVisibility(View.GONE);
- if (!response.isSuccessful()) {
- Toast.makeText(RoomDetailActivity.this, "删除失败", Toast.LENGTH_SHORT).show();
- return;
- }
- Toast.makeText(RoomDetailActivity.this, "已删除", Toast.LENGTH_SHORT).show();
- finish();
- }
-
- @Override
- public void onFailure(Call> call, Throwable t) {
- binding.loading.setVisibility(View.GONE);
- Toast.makeText(RoomDetailActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
- }
- });
- }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.java b/android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.java
index 3b8c3da9..b0ded619 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.java
@@ -1,5 +1,8 @@
package com.example.livestreaming.net;
+import android.os.Build;
+import android.util.Log;
+
import com.example.livestreaming.BuildConfig;
import okhttp3.OkHttpClient;
@@ -9,12 +12,40 @@ import retrofit2.converter.gson.GsonConverterFactory;
public final class ApiClient {
+ private static final String TAG = "ApiClient";
private static volatile Retrofit retrofit;
private static volatile ApiService service;
private ApiClient() {
}
+ /**
+ * 检测是否在模拟器中运行
+ */
+ private static boolean isEmulator() {
+ return Build.FINGERPRINT.startsWith("generic")
+ || Build.FINGERPRINT.startsWith("unknown")
+ || Build.MODEL.contains("google_sdk")
+ || Build.MODEL.contains("Emulator")
+ || Build.MODEL.contains("Android SDK built for x86")
+ || Build.MANUFACTURER.contains("Genymotion")
+ || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
+ || "google_sdk".equals(Build.PRODUCT);
+ }
+
+ /**
+ * 获取API基础地址,自动根据设备类型选择
+ */
+ private static String getBaseUrl() {
+ if (isEmulator()) {
+ Log.d(TAG, "检测到模拟器,使用模拟器API地址");
+ return BuildConfig.API_BASE_URL_EMULATOR;
+ } else {
+ Log.d(TAG, "检测到真机,使用真机API地址");
+ return BuildConfig.API_BASE_URL_DEVICE;
+ }
+ }
+
public static ApiService getService() {
if (service != null) return service;
synchronized (ApiClient.class) {
@@ -23,15 +54,20 @@ public final class ApiClient {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
- OkHttpClient client = new OkHttpClient.Builder()
+ OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder()
.addInterceptor(logging)
- .connectTimeout(5, java.util.concurrent.TimeUnit.SECONDS)
- .readTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
- .writeTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
- .build();
+ .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
+ .readTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
+ .writeTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
+ .retryOnConnectionFailure(true);
+
+ OkHttpClient client = clientBuilder.build();
+
+ String baseUrl = getBaseUrl();
+ Log.d(TAG, "API Base URL: " + baseUrl);
retrofit = new Retrofit.Builder()
- .baseUrl(BuildConfig.API_BASE_URL)
+ .baseUrl(baseUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
diff --git a/android-app/app/src/main/res/drawable/bg_white_16.xml b/android-app/app/src/main/res/drawable/bg_white_16.xml
index 6d2cc53e..617c7147 100644
--- a/android-app/app/src/main/res/drawable/bg_white_16.xml
+++ b/android-app/app/src/main/res/drawable/bg_white_16.xml
@@ -1,5 +1,7 @@
-
-
+
+
-
+
+
\ No newline at end of file
diff --git a/live-streaming/data/rooms.json b/live-streaming/data/rooms.json
index 8876abec..2904677f 100644
--- a/live-streaming/data/rooms.json
+++ b/live-streaming/data/rooms.json
@@ -18,5 +18,15 @@
"viewerCount": 0,
"createdAt": "2025-12-16T09:50:13.396Z",
"startedAt": null
+ },
+ {
+ "id": "704351b7-1ea9-445e-ba6f-08cbb2cf0b18",
+ "title": "V哈哈",
+ "streamerName": "刚刚",
+ "streamKey": "704351b7-1ea9-445e-ba6f-08cbb2cf0b18",
+ "isLive": false,
+ "viewerCount": 0,
+ "createdAt": "2025-12-17T10:44:50.964Z",
+ "startedAt": null
}
]
\ No newline at end of file