diff --git a/Zhibo/admin/src/router/modules/wishtreeManage.js b/Zhibo/admin/src/router/modules/wishtreeManage.js index cba7eed2..df4369be 100644 --- a/Zhibo/admin/src/router/modules/wishtreeManage.js +++ b/Zhibo/admin/src/router/modules/wishtreeManage.js @@ -13,25 +13,25 @@ const wishtreeManageRouter = { children: [ { path: 'festival', - component: () => import('@/views/wishtree/festival/index'), + component: () => import('@/views/wishTree/festival/index'), name: 'WishtreeFestival', meta: { title: '节日管理' }, }, { path: 'wish', - component: () => import('@/views/wishtree/wish/index'), + component: () => import('@/views/wishTree/wish/index'), name: 'WishtreeWish', meta: { title: '心愿管理' }, }, { path: 'background', - component: () => import('@/views/wishtree/background/index'), + component: () => import('@/views/wishTree/background/index'), name: 'WishtreeBackground', meta: { title: '背景素材' }, }, { path: 'statistics', - component: () => import('@/views/wishtree/statistics/index'), + component: () => import('@/views/wishTree/statistics/index'), name: 'WishtreeStatistics', meta: { title: '数据统计' }, }, diff --git a/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/task/live/LiveStatusSyncTask.java b/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/task/live/LiveStatusSyncTask.java index 121135c4..e90f1128 100644 --- a/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/task/live/LiveStatusSyncTask.java +++ b/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/task/live/LiveStatusSyncTask.java @@ -38,18 +38,27 @@ public class LiveStatusSyncTask { private final ObjectMapper objectMapper = new ObjectMapper(); /** - * 同步直播状态(每 5 秒执行一次) + * 同步直播状态(每 30 秒执行一次,减少频率) */ - @Scheduled(fixedRate = 5000) + @Scheduled(fixedRate = 30000) public void syncLiveStatus() { try { Set liveStreamKeys = fetchLiveStreamKeysFromSrs(); - updateLiveStatus(liveStreamKeys); + // 只有成功获取到 SRS 数据时才更新状态 + // 如果 SRS 查询失败(返回空集合),不修改现有状态 + if (liveStreamKeys != null && !liveStreamKeys.isEmpty()) { + updateLiveStatus(liveStreamKeys); + } else { + logger.debug("SRS 返回空流列表,保持现有直播状态不变"); + } } catch (Exception e) { logger.error("LiveStatusSyncTask 执行失败: {}", e.getMessage()); } } + // 标记是否成功连接过 SRS + private boolean srsConnected = false; + /** * 从 SRS API 获取当前正在推流的 streamKey 列表 */ @@ -63,6 +72,7 @@ public class LiveStatusSyncTask { conn.setReadTimeout(3000); if (conn.getResponseCode() == 200) { + srsConnected = true; JsonNode root = objectMapper.readTree(conn.getInputStream()); JsonNode streams = root.get("streams"); if (streams != null && streams.isArray()) { @@ -80,6 +90,8 @@ public class LiveStatusSyncTask { conn.disconnect(); } catch (Exception e) { logger.warn("查询 SRS API 失败: {}", e.getMessage()); + // 返回 null 表示查询失败,不应该修改状态 + return null; } return streamKeys; } @@ -88,6 +100,8 @@ public class LiveStatusSyncTask { * 更新数据库中的直播状态 */ private void updateLiveStatus(Set liveStreamKeys) { + if (liveStreamKeys == null) return; + List allRooms = liveRoomService.list(new LambdaQueryWrapper<>()); for (LiveRoom room : allRooms) { String streamKey = room.getStreamKey(); diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml b/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml index 24d59a51..ba09ca90 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml +++ b/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml @@ -29,6 +29,12 @@ server: max-threads: 1000 # 最大线程数量 默认200 min-spare-threads: 30 # 初始化启动线程数量 +# ============ 直播流服务器配置 ============ +# 直播流始终使用远程SRS服务器 +LIVE_PUBLIC_SRS_HOST: 1.15.149.240 +LIVE_PUBLIC_SRS_RTMP_PORT: 25002 +LIVE_PUBLIC_SRS_HTTP_PORT: 25003 + spring: profiles: # 配置的环境 diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts index 524483d9..d76c0dcc 100644 --- a/android-app/app/build.gradle.kts +++ b/android-app/app/build.gradle.kts @@ -31,16 +31,25 @@ android { } } + // ============ 主API地址(普通业务功能)============ val apiBaseUrlEmulator = (localProps.getProperty("api.base_url_emulator") ?: "http://10.0.2.2:8081/").trim() - val apiBaseUrlDevice = (localProps.getProperty("api.base_url_device") ?: "http://192.168.1.164:8081/").trim() - // 模拟器使用服务器地址 buildConfigField("String", "API_BASE_URL_EMULATOR", "\"$apiBaseUrlEmulator\"") - // 真机使用服务器地址 buildConfigField("String", "API_BASE_URL_DEVICE", "\"$apiBaseUrlDevice\"") + + // ============ 直播/通话服务地址(始终远程)============ + val liveServerHost = (localProps.getProperty("live.server_host") ?: "1.15.149.240").trim() + val liveServerPort = (localProps.getProperty("live.server_port") ?: "8083").trim() + val turnServerHost = (localProps.getProperty("turn.server_host") ?: "1.15.149.240").trim() + val turnServerPort = (localProps.getProperty("turn.server_port") ?: "3478").trim() + + buildConfigField("String", "LIVE_SERVER_HOST", "\"$liveServerHost\"") + buildConfigField("String", "LIVE_SERVER_PORT", "\"$liveServerPort\"") + buildConfigField("String", "TURN_SERVER_HOST", "\"$turnServerHost\"") + buildConfigField("String", "TURN_SERVER_PORT", "\"$turnServerPort\"") } buildTypes { diff --git a/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java b/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java index 22d2e722..0b6804d1 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java +++ b/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java @@ -39,6 +39,7 @@ public class LiveStreamingApplication extends Application { /** * 如果用户已登录,连接通话信令服务器 + * 延迟执行,避免启动时阻塞 */ public void connectCallSignalingIfLoggedIn() { String userId = AuthStore.getUserId(this); @@ -48,8 +49,15 @@ public class LiveStreamingApplication extends Application { try { int uid = (int) Double.parseDouble(userId); if (uid > 0) { - Log.d(TAG, "用户已登录,连接通话信令服务器,userId: " + uid); - CallManager.getInstance(this).connect(uid); + Log.d(TAG, "用户已登录,延迟连接通话信令服务器,userId: " + uid); + // 延迟3秒连接,避免启动时阻塞 + new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> { + try { + CallManager.getInstance(this).connect(uid); + } catch (Exception e) { + Log.e(TAG, "连接通话信令服务器失败", e); + } + }, 3000); } } catch (NumberFormatException e) { Log.e(TAG, "解析用户ID失败: " + userId, e); @@ -64,7 +72,11 @@ public class LiveStreamingApplication extends Application { */ public void onUserLoggedIn(int userId) { Log.d(TAG, "用户登录成功,连接通话信令服务器,userId: " + userId); - CallManager.getInstance(this).connect(userId); + try { + CallManager.getInstance(this).connect(userId); + } catch (Exception e) { + Log.e(TAG, "连接通话信令服务器失败", e); + } } /** diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java index d1d9dd38..f10f929b 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java @@ -103,9 +103,17 @@ public class MainActivity extends AppCompatActivity { binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - // 强制设置正确的 API 地址 - ApiClient.setCustomBaseUrl(getApplicationContext(), "http://192.168.1.164:8081/"); + // 清除之前缓存的自定义API地址,使用BuildConfig中的配置 + ApiClient.clearCustomBaseUrl(getApplicationContext()); ApiClient.getService(getApplicationContext()); + + // 调试:打印当前使用的 API 地址 + String currentApiUrl = ApiClient.getCurrentBaseUrl(getApplicationContext()); + Log.d(TAG, "========== API 配置 =========="); + Log.d(TAG, "当前 API 地址: " + currentApiUrl); + Log.d(TAG, "BuildConfig EMULATOR: " + com.example.livestreaming.BuildConfig.API_BASE_URL_EMULATOR); + Log.d(TAG, "BuildConfig DEVICE: " + com.example.livestreaming.BuildConfig.API_BASE_URL_DEVICE); + Log.d(TAG, "=============================="); // 立即显示缓存数据,提升启动速度 setupRecyclerView(); diff --git a/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.java b/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.java index 8e42427a..288bc73c 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.java @@ -78,33 +78,36 @@ public class PlayerActivity extends AppCompatActivity { releaseExoPlayer(); triedAltUrl = false; - // 创建低延迟播放器配置 + // 优化缓冲配置,平衡延迟和流畅度 + androidx.media3.exoplayer.DefaultLoadControl loadControl = + new androidx.media3.exoplayer.DefaultLoadControl.Builder() + .setBufferDurationsMs( + 3000, // 最小缓冲 3秒 + 15000, // 最大缓冲 15秒 + 1500, // 播放前缓冲 1.5秒 + 3000 // 重新缓冲 3秒 + ) + .setPrioritizeTimeOverSizeThresholds(true) + .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()) + .setLoadControl(loadControl) .build(); - // 设置播放器视图 binding.playerView.setPlayer(exo); - - // 启用低延迟模式 binding.playerView.setUseController(true); binding.playerView.setControllerAutoShow(false); String computedAltUrl = altUrl; - if (computedAltUrl == null || computedAltUrl.trim().isEmpty()) computedAltUrl = getAltHlsUrl(url); + if (computedAltUrl == null || computedAltUrl.trim().isEmpty()) { + computedAltUrl = getAltHlsUrl(url); + } String finalComputedAltUrl = computedAltUrl; exo.addListener(new Player.Listener() { @Override public void onPlayerError(PlaybackException error) { + android.util.Log.e("PlayerActivity", "播放错误: " + error.getMessage()); if (triedAltUrl) return; if (finalComputedAltUrl == null || finalComputedAltUrl.trim().isEmpty()) return; triedAltUrl = true; @@ -115,6 +118,7 @@ public class PlayerActivity extends AppCompatActivity { } }); + android.util.Log.d("PlayerActivity", "开始播放: " + url); exo.setMediaItem(MediaItem.fromUri(url)); exo.prepare(); exo.setPlayWhenReady(true); @@ -138,47 +142,16 @@ public class PlayerActivity extends AppCompatActivity { } private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) { - ensureIjkLibsLoaded(); - releaseExoPlayer(); - releaseIjkPlayer(); - - ijkUrl = flvUrl; - ijkFallbackHlsUrl = fallbackHlsUrl; - ijkFallbackTried = false; - - if (binding != null) { - binding.playerView.setVisibility(android.view.View.GONE); - binding.flvTextureView.setVisibility(android.view.View.VISIBLE); - } - - TextureView view = binding.flvTextureView; - TextureView.SurfaceTextureListener listener = new TextureView.SurfaceTextureListener() { - @Override - public void onSurfaceTextureAvailable(android.graphics.SurfaceTexture surfaceTexture, int width, int height) { - ijkSurface = new Surface(surfaceTexture); - prepareIjk(flvUrl); - } - - @Override - public void onSurfaceTextureSizeChanged(android.graphics.SurfaceTexture surface, int width, int height) { - } - - @Override - public boolean onSurfaceTextureDestroyed(android.graphics.SurfaceTexture surface) { - releaseIjkPlayer(); - return true; - } - - @Override - public void onSurfaceTextureUpdated(android.graphics.SurfaceTexture surface) { - } - }; - - view.setSurfaceTextureListener(listener); - if (view.isAvailable() && view.getSurfaceTexture() != null) { - ijkSurface = new Surface(view.getSurfaceTexture()); - prepareIjk(flvUrl); + // 禁用 IjkPlayer,直接使用 HLS 播放(IjkPlayer 在某些设备上会崩溃) + android.util.Log.d("PlayerActivity", "FLV流转为HLS播放: " + flvUrl); + + // 将 FLV 地址转换为 HLS 地址 + String hlsUrl = fallbackHlsUrl; + if (hlsUrl == null || hlsUrl.trim().isEmpty()) { + hlsUrl = flvUrl.replace(".flv", ".m3u8"); } + + startHls(hlsUrl, null); } private void prepareIjk(String url) { 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 eb0afe5e..7c6759b6 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 @@ -24,6 +24,7 @@ import androidx.media3.exoplayer.ExoPlayer; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.GridLayoutManager; +import com.example.livestreaming.call.WebRTCConfig; import com.example.livestreaming.databinding.ActivityRoomDetailNewBinding; import com.example.livestreaming.net.ApiClient; import com.example.livestreaming.net.ApiResponse; @@ -105,22 +106,25 @@ public class RoomDetailActivity extends AppCompatActivity { private WebSocket onlineCountWebSocket; private OkHttpClient onlineCountWsClient; - // 动态获取WebSocket URL + // 动态获取WebSocket URL - 直播服务使用远程服务器 private String getWsChatBaseUrl() { - String baseUrl = ApiClient.getCurrentBaseUrl(this); - if (baseUrl == null || baseUrl.isEmpty()) { - baseUrl = "http://192.168.1.164:8081/"; + try { + // 直播弹幕WebSocket使用远程服务器 + return WebRTCConfig.getLiveWsUrl() + "ws/live/chat/"; + } catch (Exception e) { + android.util.Log.e("RoomDetail", "获取WsChatBaseUrl失败", e); + return "ws://1.15.149.240:8083/ws/live/chat/"; } - // 将 http:// 转换为 ws:// - return baseUrl.replace("http://", "ws://").replace("https://", "wss://") + "ws/live/chat/"; } private String getWsOnlineBaseUrl() { - String baseUrl = ApiClient.getCurrentBaseUrl(this); - if (baseUrl == null || baseUrl.isEmpty()) { - baseUrl = "http://192.168.1.164:8081/"; + try { + // 直播在线人数WebSocket使用远程服务器 + return WebRTCConfig.getLiveWsUrl() + "ws/live/"; + } catch (Exception e) { + android.util.Log.e("RoomDetail", "获取WsOnlineBaseUrl失败", e); + return "ws://1.15.149.240:8083/ws/live/"; } - return baseUrl.replace("http://", "ws://").replace("https://", "wss://") + "ws/live/"; } // WebSocket 心跳检测 - 弹幕 @@ -150,25 +154,37 @@ public class RoomDetailActivity extends AppCompatActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // 隐藏ActionBar,使用自定义顶部栏 - if (getSupportActionBar() != null) { - getSupportActionBar().hide(); + try { + // 隐藏ActionBar,使用自定义顶部栏 + if (getSupportActionBar() != null) { + getSupportActionBar().hide(); + } + + binding = ActivityRoomDetailNewBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + ApiClient.getService(getApplicationContext()); + + roomId = getIntent().getStringExtra(EXTRA_ROOM_ID); + if (TextUtils.isEmpty(roomId)) { + Toast.makeText(this, "直播间ID无效", Toast.LENGTH_SHORT).show(); + finish(); + return; + } + + triedAltUrl = false; + + setupUI(); + setupChat(); + setupGifts(); + + // 记录观看历史(异步,不阻塞) + recordWatchHistory(); + } catch (Exception e) { + android.util.Log.e("RoomDetail", "onCreate异常: " + e.getMessage(), e); + Toast.makeText(this, "加载直播间失败", Toast.LENGTH_SHORT).show(); + finish(); } - - binding = ActivityRoomDetailNewBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - ApiClient.getService(getApplicationContext()); - - roomId = getIntent().getStringExtra(EXTRA_ROOM_ID); - triedAltUrl = false; - - setupUI(); - setupChat(); - setupGifts(); - - // 记录观看历史 - recordWatchHistory(); } private void setupUI() { @@ -744,36 +760,39 @@ public class RoomDetailActivity extends AppCompatActivity { * 记录观看历史 */ private void recordWatchHistory() { - if (!AuthHelper.isLoggedIn(this)) { - return; // 未登录用户不记录 - } - - if (TextUtils.isEmpty(roomId)) { - return; - } - - ApiService apiService = ApiClient.getService(getApplicationContext()); - - java.util.Map body = new java.util.HashMap<>(); - body.put("roomId", roomId); - body.put("watchTime", System.currentTimeMillis()); - - Call>> call = apiService.recordWatchHistory(body); - - call.enqueue(new Callback>>() { - @Override - public void onResponse(Call>> call, - Response>> response) { - if (response.isSuccessful() && response.body() != null) { - android.util.Log.d("RoomDetail", "观看历史记录成功"); + try { + if (!AuthHelper.isLoggedIn(this)) { + return; // 未登录用户不记录 + } + + if (TextUtils.isEmpty(roomId)) { + return; + } + + ApiService apiService = ApiClient.getService(getApplicationContext()); + + java.util.Map body = new java.util.HashMap<>(); + body.put("roomId", roomId); + body.put("watchTime", System.currentTimeMillis()); + + Call>> call = apiService.recordWatchHistory(body); + + call.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + // 忽略结果,接口可能不存在 } - } - @Override - public void onFailure(Call>> call, Throwable t) { - android.util.Log.e("RoomDetail", "观看历史记录失败: " + t.getMessage()); - } - }); + @Override + public void onFailure(Call>> call, Throwable t) { + // 忽略错误,接口可能不存在 + } + }); + } catch (Exception e) { + // 忽略所有异常,不影响直播观看 + android.util.Log.w("RoomDetail", "记录观看历史失败: " + e.getMessage()); + } } /** @@ -896,73 +915,92 @@ public class RoomDetailActivity extends AppCompatActivity { } 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); + try { + String title = r.getTitle() != null ? r.getTitle() : "直播间"; + String streamer = r.getStreamerName() != null ? r.getStreamerName() : "主播"; - // 在线人数已通过WebSocket实时更新,这里只设置初始值 - if (r.getViewerCount() > 0) { - binding.topViewerCount.setText(String.valueOf(r.getViewerCount())); + // 设置顶部标题栏 + binding.topTitle.setText(title); + + // 设置房间信息区域 + binding.roomTitle.setText(title); + binding.streamerName.setText(streamer); + + // 设置直播状态 + if (r.isLive()) { + binding.liveTag.setVisibility(View.VISIBLE); + binding.offlineLayout.setVisibility(View.GONE); + + // 在线人数已通过WebSocket实时更新,这里只设置初始值 + if (r.getViewerCount() > 0) { + binding.topViewerCount.setText(String.valueOf(r.getViewerCount())); + } + // 如果后端返回的viewerCount为0,保持UI不变,等待WebSocket推送 + } else { + binding.liveTag.setVisibility(View.GONE); + binding.offlineLayout.setVisibility(View.VISIBLE); + releasePlayer(); + return; } - // 如果后端返回的viewerCount为0,保持UI不变,等待WebSocket推送 - } 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; - } + // 获取播放地址 + 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(); + if (!TextUtils.isEmpty(playUrl)) { + ensurePlayer(playUrl, fallbackHlsUrl); + } else { + // 没有播放地址时显示离线状态 + binding.offlineLayout.setVisibility(View.VISIBLE); + releasePlayer(); + } + } catch (Exception e) { + android.util.Log.e("RoomDetail", "bindRoom异常: " + e.getMessage(), e); + // 不要因为绑定失败就退出,显示错误状态 + if (binding != null && binding.offlineLayout != null) { + binding.offlineLayout.setVisibility(View.VISIBLE); + } } } private void ensurePlayer(String url, String fallbackHlsUrl) { - if (TextUtils.isEmpty(url)) return; + try { + if (TextUtils.isEmpty(url)) return; - if (url.endsWith(".flv")) { - if (TextUtils.equals(ijkUrl, url) && ijkPlayer != null) return; - startFlv(url, fallbackHlsUrl); - return; + if (url.endsWith(".flv")) { + if (TextUtils.equals(ijkUrl, url) && ijkPlayer != null) return; + startFlv(url, fallbackHlsUrl); + return; + } + + if (player != null) { + MediaItem current = player.getCurrentMediaItem(); + String currentUri = current != null && current.localConfiguration != null && current.localConfiguration.uri != null + ? current.localConfiguration.uri.toString() + : null; + + if (currentUri != null && currentUri.equals(url)) return; + } + + startHls(url, null); + } catch (Exception e) { + android.util.Log.e("RoomDetail", "ensurePlayer异常: " + e.getMessage(), e); + // 播放器初始化失败,显示离线状态 + if (binding != null && binding.offlineLayout != null) { + binding.offlineLayout.setVisibility(View.VISIBLE); + } } - - 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 boolean hasShownConnectedMessage = false; + private void startHls(String url, @Nullable String altUrl) { releaseIjkPlayer(); if (binding != null) { @@ -972,46 +1010,95 @@ public class RoomDetailActivity extends AppCompatActivity { releaseExoPlayer(); triedAltUrl = false; + hasShownConnectedMessage = false; // 重置连接消息标志 - ExoPlayer exo = new ExoPlayer.Builder(this) - .setLoadControl(new androidx.media3.exoplayer.DefaultLoadControl.Builder() - .setBufferDurationsMs( - 1000, - 3000, - 500, - 1000 - ) - .build()) + // 优化缓冲配置 - 增大缓冲区以减少卡顿 + // HLS 直播通常有 2-3 秒的切片延迟,需要足够的缓冲 + androidx.media3.exoplayer.DefaultLoadControl loadControl = + new androidx.media3.exoplayer.DefaultLoadControl.Builder() + .setBufferDurationsMs( + 10000, // 最小缓冲 10秒(保证流畅播放) + 30000, // 最大缓冲 30秒(足够应对网络波动) + 5000, // 播放前缓冲 5秒(确保有足够数据再开始) + 10000 // 重新缓冲 10秒(卡顿后充分缓冲再继续) + ) + .setPrioritizeTimeOverSizeThresholds(true) .build(); + // 创建播放器 + ExoPlayer exo = new ExoPlayer.Builder(this) + .setLoadControl(loadControl) + .build(); + + // 设置播放器视图 binding.playerView.setPlayer(exo); + binding.playerView.setUseController(true); + binding.playerView.setControllerAutoShow(false); String computedAltUrl = altUrl; if (TextUtils.isEmpty(computedAltUrl)) computedAltUrl = getAltHlsUrl(url); String finalComputedAltUrl = computedAltUrl; + final String finalUrl = url; + exo.addListener(new Player.Listener() { + private int retryCount = 0; + private static final int MAX_RETRY = 3; + @Override public void onPlayerError(PlaybackException error) { - if (triedAltUrl || TextUtils.isEmpty(finalComputedAltUrl)) { - binding.offlineLayout.setVisibility(View.VISIBLE); + android.util.Log.e("ExoPlayer", "播放错误: " + error.getMessage()); + + // 先尝试备用地址 + if (!triedAltUrl && !TextUtils.isEmpty(finalComputedAltUrl)) { + triedAltUrl = true; + android.util.Log.d("ExoPlayer", "尝试备用地址: " + finalComputedAltUrl); + exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl)); + exo.prepare(); + exo.setPlayWhenReady(true); return; } - triedAltUrl = true; - exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl)); - exo.prepare(); - exo.setPlayWhenReady(true); + + // 自动重试 + if (retryCount < MAX_RETRY) { + retryCount++; + android.util.Log.d("ExoPlayer", "自动重试 " + retryCount + "/" + MAX_RETRY); + handler.postDelayed(() -> { + if (!isFinishing() && !isDestroyed()) { + exo.setMediaItem(MediaItem.fromUri(finalUrl)); + exo.prepare(); + exo.setPlayWhenReady(true); + } + }, 2000); + return; + } + + // 重试失败,显示离线状态 + binding.offlineLayout.setVisibility(View.VISIBLE); + handler.postDelayed(() -> { + if (!isFinishing() && !isDestroyed()) { + fetchRoom(); + } + }, 5000); } @Override public void onPlaybackStateChanged(int playbackState) { if (playbackState == Player.STATE_READY) { binding.offlineLayout.setVisibility(View.GONE); - addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true)); + retryCount = 0; + // 只显示一次连接消息 + if (!hasShownConnectedMessage) { + hasShownConnectedMessage = true; + addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true)); + } + } else if (playbackState == Player.STATE_BUFFERING) { + android.util.Log.d("ExoPlayer", "正在缓冲..."); } } }); + android.util.Log.d("ExoPlayer", "开始播放: " + url); exo.setMediaItem(MediaItem.fromUri(url)); exo.prepare(); exo.setPlayWhenReady(true); @@ -1019,47 +1106,16 @@ public class RoomDetailActivity extends AppCompatActivity { } 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); + android.util.Log.d("RoomDetail", "开始播放FLV流: " + flvUrl); + + // 直接使用 HLS 播放,避免 IjkPlayer 崩溃问题 + // HLS 虽然延迟稍高,但稳定性更好 + String hlsUrl = fallbackHlsUrl; + if (TextUtils.isEmpty(hlsUrl)) { + hlsUrl = flvUrl.replace(".flv", ".m3u8"); } + android.util.Log.d("RoomDetail", "使用 HLS 播放: " + hlsUrl); + startHls(hlsUrl, null); } private void prepareIjk(String url) { @@ -1067,36 +1123,39 @@ public class RoomDetailActivity extends AppCompatActivity { IjkMediaPlayer p = new IjkMediaPlayer(); // 优化缓冲设置,减少卡顿 - p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1); // 开启缓冲 + p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1); p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1); p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer"); - p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 100000); // 100ms分析时长 - p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240); // 10KB探测大小 - p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 5); // 允许丢帧 - p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000); // 3秒缓存 - p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min_frames", 50); // 最小缓冲帧数 - p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 0); // 关闭无限缓冲 - p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1); // 断线重连 + p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 100000); + p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240); + p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 5); + p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000); + p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min_frames", 50); + p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 0); + p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1); p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_streamed", 1); - p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_delay_max", 5); // 最大重连延迟5秒 + p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_delay_max", 5); p.setOnPreparedListener(mp -> { binding.offlineLayout.setVisibility(View.GONE); mp.start(); - addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true)); - }); + // 只显示一次连接消息 + if (!hasShownConnectedMessage) { + hasShownConnectedMessage = true; + addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true)); + } + }); - p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> { - android.util.Log.e("IjkPlayer", "播放错误: what=" + what + ", extra=" + extra); - if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) { - binding.offlineLayout.setVisibility(View.VISIBLE); - // 5秒后尝试重新连接 - handler.postDelayed(() -> { - if (!isFinishing() && !isDestroyed() && room != null && room.isLive()) { - fetchRoom(); // 重新获取房间信息并播放 - } - }, 5000); - return true; + p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> { + android.util.Log.e("IjkPlayer", "播放错误: what=" + what + ", extra=" + extra); + if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) { + binding.offlineLayout.setVisibility(View.VISIBLE); + handler.postDelayed(() -> { + if (!isFinishing() && !isDestroyed() && room != null && room.isLive()) { + fetchRoom(); + } + }, 5000); + return true; } ijkFallbackTried = true; startHls(ijkFallbackHlsUrl, null); @@ -1133,14 +1192,34 @@ public class RoomDetailActivity extends AppCompatActivity { } } + private static boolean ijkLibLoadFailed = false; + private static void ensureIjkLibsLoaded() { - if (ijkLibLoaded) return; + if (ijkLibLoaded || ijkLibLoadFailed) return; try { + // 检查设备 CPU 架构 + String[] abis = android.os.Build.SUPPORTED_ABIS; + boolean supported = false; + for (String abi : abis) { + if ("armeabi-v7a".equals(abi) || "arm64-v8a".equals(abi)) { + supported = true; + break; + } + } + if (!supported) { + android.util.Log.w("IjkPlayer", "设备 CPU 架构不支持 IjkPlayer: " + java.util.Arrays.toString(abis)); + ijkLibLoadFailed = true; + return; + } + IjkMediaPlayer.loadLibrariesOnce(null); IjkMediaPlayer.native_profileBegin("libijkplayer.so"); - } catch (Throwable ignored) { + ijkLibLoaded = true; + android.util.Log.d("IjkPlayer", "IjkPlayer 库加载成功"); + } catch (Throwable e) { + android.util.Log.e("IjkPlayer", "加载IjkPlayer库失败: " + e.getMessage()); + ijkLibLoadFailed = true; } - ijkLibLoaded = true; } private void releasePlayer() { diff --git a/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java b/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java index 51c9c979..46ba96fe 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java +++ b/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java @@ -81,6 +81,7 @@ public class CallManager implements CallSignalingClient.SignalingListener { /** * 连接信令服务器 + * 通话服务始终使用远程服务器地址 */ public void connect(int userId) { Log.d(TAG, "connect() called, userId: " + userId); @@ -88,8 +89,9 @@ public class CallManager implements CallSignalingClient.SignalingListener { Log.d(TAG, "已经连接,跳过"); return; } - String baseUrl = ApiClient.getCurrentBaseUrl(context); - Log.d(TAG, "连接信令服务器,baseUrl: " + baseUrl); + // 通话服务使用独立的远程服务器地址 + String baseUrl = WebRTCConfig.getLiveServerUrl(); + Log.d(TAG, "连接信令服务器(远程),baseUrl: " + baseUrl); signalingClient = new CallSignalingClient(baseUrl, userId); signalingClient.setListener(this); signalingClient.connect(); diff --git a/android-app/app/src/main/java/com/example/livestreaming/call/WebRTCConfig.java b/android-app/app/src/main/java/com/example/livestreaming/call/WebRTCConfig.java index 306ce122..0cff614c 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/call/WebRTCConfig.java +++ b/android-app/app/src/main/java/com/example/livestreaming/call/WebRTCConfig.java @@ -1,24 +1,23 @@ package com.example.livestreaming.call; +import com.example.livestreaming.BuildConfig; + /** * WebRTC 配置 - * 部署时修改这里的服务器地址 + * 直播/通话服务始终连接远程服务器 + * 配置在 local.properties 中修改 */ public class WebRTCConfig { // ============ STUN 服务器 ============ - // STUN 用于获取设备的公网IP,帮助建立P2P连接 public static final String[] STUN_SERVERS = { - "stun:stun.l.google.com:19302", // Google STUN(国内可能不稳定) - "stun:stun.qq.com:3478", // 腾讯 STUN(国内推荐) - "stun:stun.miwifi.com:3478" // 小米 STUN(国内推荐) + "stun:stun.l.google.com:19302", + "stun:stun.qq.com:3478", + "stun:stun.miwifi.com:3478" }; - // ============ TURN 服务器 ============ - // TURN 用于在P2P连接失败时进行中继转发 - - // 你的服务器TURN地址 - public static final String TURN_SERVER_URL = "turn:1.15.149.240:3478"; + // ============ TURN 服务器(从BuildConfig读取)============ + public static final String TURN_SERVER_URL = "turn:" + BuildConfig.TURN_SERVER_HOST + ":" + BuildConfig.TURN_SERVER_PORT; // TURN 服务器用户名 public static final String TURN_USERNAME = "turnuser"; @@ -26,27 +25,12 @@ public class WebRTCConfig { // TURN 服务器密码 public static final String TURN_PASSWORD = "TurnPass123456"; - // ============ 使用说明 ============ - /* - * 局域网测试: - * - 不需要修改,当前配置即可使用 - * - * 部署到公网服务器: - * 1. 在服务器安装 coturn (TURN服务器) - * 2. 修改上面的配置: - * TURN_SERVER_URL = "turn:你的服务器IP:3478" - * TURN_USERNAME = "你设置的用户名" - * TURN_PASSWORD = "你设置的密码" - * - * 宝塔安装 coturn 步骤: - * 1. SSH执行: yum install -y coturn (CentOS) 或 apt install -y coturn (Ubuntu) - * 2. 编辑 /etc/turnserver.conf: - * listening-port=3478 - * external-ip=你的公网IP - * realm=你的公网IP - * lt-cred-mech - * user=用户名:密码 - * 3. 宝塔放行端口: 3478(TCP/UDP), 49152-65535(UDP) - * 4. 启动: systemctl start coturn - */ + // ============ 直播/通话服务地址 ============ + public static String getLiveServerUrl() { + return "http://" + BuildConfig.LIVE_SERVER_HOST + ":" + BuildConfig.LIVE_SERVER_PORT + "/"; + } + + public static String getLiveWsUrl() { + return "ws://" + BuildConfig.LIVE_SERVER_HOST + ":" + BuildConfig.LIVE_SERVER_PORT + "/"; + } } diff --git a/android-app/app/src/main/res/drawable/bg_btn_cancel.xml b/android-app/app/src/main/res/drawable/bg_btn_cancel.xml new file mode 100644 index 00000000..7585b5a5 --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_btn_cancel.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-app/app/src/main/res/drawable/bg_btn_primary.xml b/android-app/app/src/main/res/drawable/bg_btn_primary.xml new file mode 100644 index 00000000..d1c114a6 --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_btn_primary.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-app/app/src/main/res/drawable/bg_dialog_rounded.xml b/android-app/app/src/main/res/drawable/bg_dialog_rounded.xml new file mode 100644 index 00000000..ca7d6bfc --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_dialog_rounded.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-app/app/src/main/res/drawable/bg_edit_text.xml b/android-app/app/src/main/res/drawable/bg_edit_text.xml new file mode 100644 index 00000000..208827ec --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_edit_text.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android-app/app/src/main/res/drawable/bg_wish_content.xml b/android-app/app/src/main/res/drawable/bg_wish_content.xml new file mode 100644 index 00000000..5b16e519 --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_wish_content.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android-app/app/src/main/res/layout/dialog_make_wish.xml b/android-app/app/src/main/res/layout/dialog_make_wish.xml index da09d37d..85929fbb 100644 --- a/android-app/app/src/main/res/layout/dialog_make_wish.xml +++ b/android-app/app/src/main/res/layout/dialog_make_wish.xml @@ -3,7 +3,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:padding="16dp"> + android:background="@drawable/bg_dialog_rounded" + android:padding="20dp"> @@ -20,5 +22,49 @@ android:layout_height="wrap_content" android:hint="写下你的愿望..." android:minLines="3" - android:gravity="top" /> + android:maxLength="50" + android:gravity="top" + android:padding="12dp" + android:background="@drawable/bg_edit_text" /> + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/dialog_view_wish.xml b/android-app/app/src/main/res/layout/dialog_view_wish.xml new file mode 100644 index 00000000..375d2def --- /dev/null +++ b/android-app/app/src/main/res/layout/dialog_view_wish.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + diff --git a/friend_tables.sql b/friend_tables.sql new file mode 100644 index 00000000..fb2fc81b --- /dev/null +++ b/friend_tables.sql @@ -0,0 +1,60 @@ +-- 好友系统数据库表 +-- 请在服务器数据库中执行此脚本 + +-- 1. 好友关系表 +CREATE TABLE IF NOT EXISTS `eb_friend` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user_id` int(11) NOT NULL COMMENT '用户ID', + `friend_id` int(11) NOT NULL COMMENT '好友ID', + `remark` varchar(50) DEFAULT NULL COMMENT '好友备注', + `status` int(11) NOT NULL DEFAULT 1 COMMENT '状态:1-正常 0-已删除', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_friend` (`user_id`, `friend_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_friend_id` (`friend_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友关系表'; + +-- 2. 好友请求表 +CREATE TABLE IF NOT EXISTS `eb_friend_request` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `from_user_id` int(11) NOT NULL COMMENT '发起请求的用户ID', + `to_user_id` int(11) NOT NULL COMMENT '接收请求的用户ID', + `message` varchar(200) DEFAULT NULL COMMENT '验证消息', + `status` int(11) NOT NULL DEFAULT 0 COMMENT '状态:0-待处理 1-已接受 2-已拒绝', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '处理时间', + PRIMARY KEY (`id`), + KEY `idx_from_user` (`from_user_id`), + KEY `idx_to_user` (`to_user_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友请求表'; + +-- 3. 用户黑名单表 +CREATE TABLE IF NOT EXISTS `eb_user_blacklist` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user_id` int(11) NOT NULL COMMENT '用户ID', + `blocked_user_id` int(11) NOT NULL COMMENT '被拉黑的用户ID', + `reason` varchar(200) DEFAULT NULL COMMENT '拉黑原因', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_blocked` (`user_id`, `blocked_user_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_blocked_user_id` (`blocked_user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户黑名单表'; + +-- 4. 私聊会话表(如果不存在) +CREATE TABLE IF NOT EXISTS `eb_conversation` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user1_id` int(11) NOT NULL COMMENT '用户1 ID(较小的ID)', + `user2_id` int(11) NOT NULL COMMENT '用户2 ID(较大的ID)', + `last_message_id` bigint(20) DEFAULT NULL COMMENT '最后一条消息ID', + `last_message_time` datetime DEFAULT NULL COMMENT '最后消息时间', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_users` (`user1_id`, `user2_id`), + KEY `idx_user1` (`user1_id`), + KEY `idx_user2` (`user2_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私聊会话表'; diff --git a/环境配置指南.md b/环境配置指南.md new file mode 100644 index 00000000..31500047 --- /dev/null +++ b/环境配置指南.md @@ -0,0 +1,177 @@ +# 环境配置指南 + +本项目采用分离配置架构: +- **普通业务功能**(用户、消息、商城等)→ 可切换本地/远程 +- **直播/通话服务** → 始终连接远程服务器 + +--- + +## 一、APP 配置 + +### 配置文件位置 +`android-app/local.properties` + +### 配置说明 + +```properties +# ============ 主API地址(普通业务功能)============ +# 本地开发时使用本地地址 +api.base_url_emulator=http://10.0.2.2:8081/ +api.base_url_device=http://192.168.1.164:8081/ + +# ============ 直播/通话服务地址(始终远程)============ +live.server_host=1.15.149.240 +live.server_port=8083 +turn.server_host=1.15.149.240 +turn.server_port=3478 +``` + +### 切换环境 + +#### 本地开发(默认) +```properties +api.base_url_emulator=http://10.0.2.2:8081/ +api.base_url_device=http://192.168.1.164:8081/ +``` + +#### 全部连接远程 +```properties +api.base_url_emulator=http://1.15.149.240:8083/ +api.base_url_device=http://1.15.149.240:8083/ +``` + +### 修改后需要重新编译 +```bash +cd android-app +.\gradlew assembleRelease +``` + +--- + +## 二、前端管理端配置 + +### 配置文件位置 +- 开发环境:`Zhibo/admin/.env.development` +- 生产环境:`Zhibo/admin/.env.production` + +### 开发环境(本地) +```env +ENV = 'development' +VUE_APP_BASE_API = 'http://127.0.0.1:30001' +``` + +### 生产环境(远程) +```env +ENV = 'production' +VUE_APP_BASE_API = '' +``` +> 生产环境使用空字符串,由Nginx代理到后端 + +--- + +## 三、后端配置 + +### 配置文件位置 +`Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml` + +### 关键配置 + +```yaml +server: + port: 8081 # 本地开发端口 + +spring: + datasource: + url: jdbc:mysql://1.15.149.240:3306/zhibo # 数据库地址 + redis: + host: 127.0.0.1 # Redis地址 +``` + +### 服务器启动脚本 +```bash +#!/bin/bash +JAR_PATH="/www/wwwroot/1.15.149.240_30002/Jar" + +# Front API (带SRS配置) +nohup java -Xms512m -Xmx1024m -jar ${JAR_PATH}/Crmeb-front.jar \ + --server.port=8083 \ + --spring.redis.host=127.0.0.1 \ + --LIVE_PUBLIC_SRS_HOST=1.15.149.240 \ + --LIVE_PUBLIC_SRS_RTMP_PORT=25002 \ + --LIVE_PUBLIC_SRS_HTTP_PORT=25003 \ + > ${JAR_PATH}/logs/front.log 2>&1 & +``` + +--- + +## 四、服务架构图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 远程服务器 1.15.149.240 │ +├─────────────────────────────────────────────────────────────┤ +│ Front API (8083) │ +│ ├── WebSocket: /ws/call (通话信令) ←── APP直连 │ +│ ├── WebSocket: /ws/live/* (直播弹幕) ←── APP直连 │ +│ └── REST API: /api/front/* (可选) │ +│ │ +│ SRS流媒体服务器 │ +│ ├── RTMP: 25002 │ +│ └── HTTP-FLV: 25003 │ +│ │ +│ TURN服务器: 3478 │ +└─────────────────────────────────────────────────────────────┘ + ▲ + 直播/通话服务 │ + ─────────────────────┼───────────────────── + 普通业务功能 │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 本地开发环境 │ +├─────────────────────────────────────────────────────────────┤ +│ Front API (8081) │ +│ └── REST API: /api/front/* ←── APP连接 │ +│ │ +│ Admin API (30001) │ +│ └── REST API: /api/admin/* ←── 管理前端连接 │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 五、快速切换命令 + +### APP切换到全远程 +编辑 `android-app/local.properties`: +```properties +api.base_url_device=http://1.15.149.240:8083/ +``` + +### APP切换到本地开发 +编辑 `android-app/local.properties`: +```properties +api.base_url_device=http://192.168.1.164:8081/ +``` + +### 前端切换 +```bash +# 开发模式(连本地) +npm run dev + +# 生产构建(连远程) +npm run build:prod +``` + +--- + +## 六、端口清单 + +| 服务 | 本地端口 | 远程端口 | 说明 | +|------|----------|----------|------| +| Front API | 8081 | 8083 | APP主API | +| Admin API | 30001 | 30003 | 管理后台API | +| SRS RTMP | - | 25002 | 直播推流 | +| SRS HTTP | - | 25003 | 直播播放 | +| TURN | - | 3478 | 视频通话中继 | +| MySQL | - | 3306 | 数据库 | +| Redis | 6379 | 6379 | 缓存 | diff --git a/环境配置汇总.md b/环境配置汇总.md new file mode 100644 index 00000000..b8d502a9 --- /dev/null +++ b/环境配置汇总.md @@ -0,0 +1,141 @@ +# 环境配置汇总 + +## 一、Android APP 配置 + +### 1. 主配置文件:`android-app/local.properties` + +```properties +# ============ API 服务器地址配置 ============ +# 主API地址 - 模拟器使用 +api.base_url_emulator=http://1.15.149.240:8083/ +# 主API地址 - 真机使用 +api.base_url_device=http://1.15.149.240:8083/ + +# ============ 直播/通话服务地址配置 ============ +# 直播/通话 WebSocket 服务地址 +live.server_host=1.15.149.240 +live.server_port=8083 +# TURN 服务器地址(视频通话中继) +turn.server_host=1.15.149.240 +turn.server_port=3478 +``` + +**说明**: +- `api.base_url_emulator` - 模拟器运行时使用的API地址 +- `api.base_url_device` - 真机运行时使用的API地址 +- `live.server_host/port` - 直播和通话WebSocket服务地址 +- `turn.server_host/port` - WebRTC TURN中继服务器地址 + +### 2. 相关代码文件(无需修改,自动读取local.properties) + +| 文件 | 作用 | +|------|------| +| `app/build.gradle.kts` | 读取local.properties生成BuildConfig | +| `ApiConfig.java` | 提供统一的API地址获取方法 | +| `ApiClient.java` | 网络请求客户端,自动选择模拟器/真机地址 | +| `WebRTCConfig.java` | WebRTC/TURN服务器配置 | + +--- + +## 二、前端 Admin 配置 + +### 1. 开发环境:`Zhibo/admin/.env.development` + +```properties +ENV = 'development' +# 本地开发时连接本地后端 +VUE_APP_BASE_API = 'http://127.0.0.1:30001' +``` + +### 2. 生产环境:`Zhibo/admin/.env.production` + +```properties +ENV = 'production' +# 生产环境使用相对路径,由Nginx代理 +VUE_APP_BASE_API = '' +``` + +### 3. 预发布环境:`Zhibo/admin/.env.staging` + +```properties +ENV = 'production' +VUE_APP_BASE_API = 'http://192.168.31.35:2500' +``` + +**说明**: +- 开发环境:`npm run dev` 使用 `.env.development` +- 生产打包:`npm run build:prod` 使用 `.env.production` +- 预发布打包:`npm run build:stage` 使用 `.env.staging` + +--- + +## 三、环境切换指南 + +### 切换到生产环境(服务器 1.15.149.240) + +**Android APP**: +```properties +# android-app/local.properties +api.base_url_emulator=http://1.15.149.240:8083/ +api.base_url_device=http://1.15.149.240:8083/ +live.server_host=1.15.149.240 +live.server_port=8083 +turn.server_host=1.15.149.240 +turn.server_port=3478 +``` + +**前端 Admin**: +```properties +# Zhibo/admin/.env.production +VUE_APP_BASE_API = '' +``` +然后执行 `npm run build:prod` + +--- + +### 切换到本地开发环境 + +**Android APP**: +```properties +# android-app/local.properties +api.base_url_emulator=http://10.0.2.2:8081/ +api.base_url_device=http://192.168.x.x:8081/ +live.server_host=1.15.149.240 +live.server_port=8083 +turn.server_host=1.15.149.240 +turn.server_port=3478 +``` +> 注意:直播/通话服务建议始终使用远程服务器 + +**前端 Admin**: +```properties +# Zhibo/admin/.env.development +VUE_APP_BASE_API = 'http://127.0.0.1:30001' +``` +然后执行 `npm run dev` + +--- + +## 四、服务端口说明 + +| 服务 | 端口 | 说明 | +|------|------|------| +| Admin API | 30003 | 管理后台API | +| Front API | 8083 | 前端/APP API | +| Admin 前端 | 30002 | 管理后台网页 | +| TURN 服务 | 3478 | WebRTC中继 | +| SRS RTMP | 25002 | 直播推流 | +| SRS HTTP | 25003 | 直播拉流 | + +--- + +## 五、修改后需要的操作 + +### Android APP +1. 修改 `local.properties` +2. 重新编译:`./gradlew assembleRelease` + +### 前端 Admin +1. 修改对应的 `.env.*` 文件 +2. 重新打包:`npm run build:prod` +3. 上传 `dist` 目录到服务器