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