Compare commits
No commits in common. "8345dca1a1faad326eb775485bff2e1c90d09ff2" and "3c9b31185f86c7c2697d239b787c43b0e2c1a0d4" have entirely different histories.
8345dca1a1
...
3c9b31185f
|
|
@ -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: '数据统计' },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -38,27 +38,18 @@ public class LiveStatusSyncTask {
|
|||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 同步直播状态(每 30 秒执行一次,减少频率)
|
||||
* 同步直播状态(每 5 秒执行一次)
|
||||
*/
|
||||
@Scheduled(fixedRate = 30000)
|
||||
@Scheduled(fixedRate = 5000)
|
||||
public void syncLiveStatus() {
|
||||
try {
|
||||
Set<String> liveStreamKeys = fetchLiveStreamKeysFromSrs();
|
||||
// 只有成功获取到 SRS 数据时才更新状态
|
||||
// 如果 SRS 查询失败(返回空集合),不修改现有状态
|
||||
if (liveStreamKeys != null && !liveStreamKeys.isEmpty()) {
|
||||
updateLiveStatus(liveStreamKeys);
|
||||
} else {
|
||||
logger.debug("SRS 返回空流列表,保持现有直播状态不变");
|
||||
}
|
||||
updateLiveStatus(liveStreamKeys);
|
||||
} catch (Exception e) {
|
||||
logger.error("LiveStatusSyncTask 执行失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 标记是否成功连接过 SRS
|
||||
private boolean srsConnected = false;
|
||||
|
||||
/**
|
||||
* 从 SRS API 获取当前正在推流的 streamKey 列表
|
||||
*/
|
||||
|
|
@ -72,7 +63,6 @@ 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()) {
|
||||
|
|
@ -90,8 +80,6 @@ public class LiveStatusSyncTask {
|
|||
conn.disconnect();
|
||||
} catch (Exception e) {
|
||||
logger.warn("查询 SRS API 失败: {}", e.getMessage());
|
||||
// 返回 null 表示查询失败,不应该修改状态
|
||||
return null;
|
||||
}
|
||||
return streamKeys;
|
||||
}
|
||||
|
|
@ -100,8 +88,6 @@ public class LiveStatusSyncTask {
|
|||
* 更新数据库中的直播状态
|
||||
*/
|
||||
private void updateLiveStatus(Set<String> liveStreamKeys) {
|
||||
if (liveStreamKeys == null) return;
|
||||
|
||||
List<LiveRoom> allRooms = liveRoomService.list(new LambdaQueryWrapper<>());
|
||||
for (LiveRoom room : allRooms) {
|
||||
String streamKey = room.getStreamKey();
|
||||
|
|
|
|||
|
|
@ -29,12 +29,6 @@ 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:
|
||||
# 配置的环境
|
||||
|
|
|
|||
|
|
@ -31,25 +31,16 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ public class LiveStreamingApplication extends Application {
|
|||
|
||||
/**
|
||||
* 如果用户已登录,连接通话信令服务器
|
||||
* 延迟执行,避免启动时阻塞
|
||||
*/
|
||||
public void connectCallSignalingIfLoggedIn() {
|
||||
String userId = AuthStore.getUserId(this);
|
||||
|
|
@ -49,15 +48,8 @@ public class LiveStreamingApplication extends Application {
|
|||
try {
|
||||
int uid = (int) Double.parseDouble(userId);
|
||||
if (uid > 0) {
|
||||
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);
|
||||
Log.d(TAG, "用户已登录,连接通话信令服务器,userId: " + uid);
|
||||
CallManager.getInstance(this).connect(uid);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(TAG, "解析用户ID失败: " + userId, e);
|
||||
|
|
@ -72,11 +64,7 @@ public class LiveStreamingApplication extends Application {
|
|||
*/
|
||||
public void onUserLoggedIn(int userId) {
|
||||
Log.d(TAG, "用户登录成功,连接通话信令服务器,userId: " + userId);
|
||||
try {
|
||||
CallManager.getInstance(this).connect(userId);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "连接通话信令服务器失败", e);
|
||||
}
|
||||
CallManager.getInstance(this).connect(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -103,17 +103,9 @@ public class MainActivity extends AppCompatActivity {
|
|||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
// 清除之前缓存的自定义API地址,使用BuildConfig中的配置
|
||||
ApiClient.clearCustomBaseUrl(getApplicationContext());
|
||||
// 强制设置正确的 API 地址
|
||||
ApiClient.setCustomBaseUrl(getApplicationContext(), "http://192.168.1.164:8081/");
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -78,36 +78,33 @@ 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(loadControl)
|
||||
.setLoadControl(new androidx.media3.exoplayer.DefaultLoadControl.Builder()
|
||||
// 减少缓冲区大小,降低延迟
|
||||
.setBufferDurationsMs(
|
||||
1000, // 最小缓冲时长 1秒
|
||||
3000, // 最大缓冲时长 3秒
|
||||
500, // 播放缓冲时长 0.5秒
|
||||
1000 // 播放后缓冲时长 1秒
|
||||
)
|
||||
.build())
|
||||
.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;
|
||||
|
|
@ -118,7 +115,6 @@ public class PlayerActivity extends AppCompatActivity {
|
|||
}
|
||||
});
|
||||
|
||||
android.util.Log.d("PlayerActivity", "开始播放: " + url);
|
||||
exo.setMediaItem(MediaItem.fromUri(url));
|
||||
exo.prepare();
|
||||
exo.setPlayWhenReady(true);
|
||||
|
|
@ -142,16 +138,47 @@ public class PlayerActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
|
||||
// 禁用 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");
|
||||
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);
|
||||
}
|
||||
|
||||
startHls(hlsUrl, null);
|
||||
}
|
||||
|
||||
private void prepareIjk(String url) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ 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;
|
||||
|
|
@ -106,25 +105,22 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
private WebSocket onlineCountWebSocket;
|
||||
private OkHttpClient onlineCountWsClient;
|
||||
|
||||
// 动态获取WebSocket URL - 直播服务使用远程服务器
|
||||
// 动态获取WebSocket URL
|
||||
private String getWsChatBaseUrl() {
|
||||
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/";
|
||||
String baseUrl = ApiClient.getCurrentBaseUrl(this);
|
||||
if (baseUrl == null || baseUrl.isEmpty()) {
|
||||
baseUrl = "http://192.168.1.164:8081/";
|
||||
}
|
||||
// 将 http:// 转换为 ws://
|
||||
return baseUrl.replace("http://", "ws://").replace("https://", "wss://") + "ws/live/chat/";
|
||||
}
|
||||
|
||||
private String getWsOnlineBaseUrl() {
|
||||
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/";
|
||||
String baseUrl = ApiClient.getCurrentBaseUrl(this);
|
||||
if (baseUrl == null || baseUrl.isEmpty()) {
|
||||
baseUrl = "http://192.168.1.164:8081/";
|
||||
}
|
||||
return baseUrl.replace("http://", "ws://").replace("https://", "wss://") + "ws/live/";
|
||||
}
|
||||
|
||||
// WebSocket 心跳检测 - 弹幕
|
||||
|
|
@ -154,37 +150,25 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
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();
|
||||
// 隐藏ActionBar,使用自定义顶部栏
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
|
||||
binding = ActivityRoomDetailNewBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
ApiClient.getService(getApplicationContext());
|
||||
|
||||
roomId = getIntent().getStringExtra(EXTRA_ROOM_ID);
|
||||
triedAltUrl = false;
|
||||
|
||||
setupUI();
|
||||
setupChat();
|
||||
setupGifts();
|
||||
|
||||
// 记录观看历史
|
||||
recordWatchHistory();
|
||||
}
|
||||
|
||||
private void setupUI() {
|
||||
|
|
@ -760,39 +744,36 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
* 记录观看历史
|
||||
*/
|
||||
private void recordWatchHistory() {
|
||||
try {
|
||||
if (!AuthHelper.isLoggedIn(this)) {
|
||||
return; // 未登录用户不记录
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(roomId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ApiService apiService = ApiClient.getService(getApplicationContext());
|
||||
|
||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
||||
body.put("roomId", roomId);
|
||||
body.put("watchTime", System.currentTimeMillis());
|
||||
|
||||
Call<ApiResponse<java.util.Map<String, Object>>> call = apiService.recordWatchHistory(body);
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<java.util.Map<String, Object>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call,
|
||||
Response<ApiResponse<java.util.Map<String, Object>>> response) {
|
||||
// 忽略结果,接口可能不存在
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
|
||||
// 忽略错误,接口可能不存在
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
// 忽略所有异常,不影响直播观看
|
||||
android.util.Log.w("RoomDetail", "记录观看历史失败: " + e.getMessage());
|
||||
if (!AuthHelper.isLoggedIn(this)) {
|
||||
return; // 未登录用户不记录
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(roomId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ApiService apiService = ApiClient.getService(getApplicationContext());
|
||||
|
||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
||||
body.put("roomId", roomId);
|
||||
body.put("watchTime", System.currentTimeMillis());
|
||||
|
||||
Call<ApiResponse<java.util.Map<String, Object>>> call = apiService.recordWatchHistory(body);
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<java.util.Map<String, Object>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call,
|
||||
Response<ApiResponse<java.util.Map<String, Object>>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
android.util.Log.d("RoomDetail", "观看历史记录成功");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
|
||||
android.util.Log.e("RoomDetail", "观看历史记录失败: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -915,91 +896,72 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void bindRoom(Room r) {
|
||||
try {
|
||||
String title = r.getTitle() != null ? r.getTitle() : "直播间";
|
||||
String streamer = r.getStreamerName() != null ? r.getStreamerName() : "主播";
|
||||
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);
|
||||
|
||||
// 设置顶部标题栏
|
||||
binding.topTitle.setText(title);
|
||||
|
||||
// 设置房间信息区域
|
||||
binding.roomTitle.setText(title);
|
||||
binding.streamerName.setText(streamer);
|
||||
// 在线人数已通过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;
|
||||
}
|
||||
|
||||
// 设置直播状态
|
||||
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;
|
||||
}
|
||||
// 获取播放地址
|
||||
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();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("RoomDetail", "bindRoom异常: " + e.getMessage(), e);
|
||||
// 不要因为绑定失败就退出,显示错误状态
|
||||
if (binding != null && binding.offlineLayout != null) {
|
||||
binding.offlineLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
if (!TextUtils.isEmpty(playUrl)) {
|
||||
ensurePlayer(playUrl, fallbackHlsUrl);
|
||||
} else {
|
||||
// 没有播放地址时显示离线状态
|
||||
binding.offlineLayout.setVisibility(View.VISIBLE);
|
||||
releasePlayer();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensurePlayer(String url, String fallbackHlsUrl) {
|
||||
try {
|
||||
if (TextUtils.isEmpty(url)) return;
|
||||
if (TextUtils.isEmpty(url)) return;
|
||||
|
||||
if (url.endsWith(".flv")) {
|
||||
if (TextUtils.equals(ijkUrl, url) && ijkPlayer != null) return;
|
||||
startFlv(url, fallbackHlsUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (player != null) {
|
||||
MediaItem current = player.getCurrentMediaItem();
|
||||
String currentUri = current != null && current.localConfiguration != null && current.localConfiguration.uri != null
|
||||
? current.localConfiguration.uri.toString()
|
||||
: null;
|
||||
|
||||
if (currentUri != null && currentUri.equals(url)) return;
|
||||
}
|
||||
|
||||
startHls(url, null);
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("RoomDetail", "ensurePlayer异常: " + e.getMessage(), e);
|
||||
// 播放器初始化失败,显示离线状态
|
||||
if (binding != null && binding.offlineLayout != null) {
|
||||
binding.offlineLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
if (url.endsWith(".flv")) {
|
||||
if (TextUtils.equals(ijkUrl, url) && ijkPlayer != null) return;
|
||||
startFlv(url, fallbackHlsUrl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 防止重复显示连接消息
|
||||
private boolean hasShownConnectedMessage = false;
|
||||
if (player != null) {
|
||||
MediaItem current = player.getCurrentMediaItem();
|
||||
String currentUri = current != null && current.localConfiguration != null && current.localConfiguration.uri != null
|
||||
? current.localConfiguration.uri.toString()
|
||||
: null;
|
||||
|
||||
if (currentUri != null && currentUri.equals(url)) return;
|
||||
}
|
||||
|
||||
startHls(url, null);
|
||||
}
|
||||
|
||||
private void startHls(String url, @Nullable String altUrl) {
|
||||
releaseIjkPlayer();
|
||||
|
|
@ -1010,95 +972,46 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
|
||||
releaseExoPlayer();
|
||||
triedAltUrl = false;
|
||||
hasShownConnectedMessage = false; // 重置连接消息标志
|
||||
|
||||
// 优化缓冲配置 - 增大缓冲区以减少卡顿
|
||||
// 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)
|
||||
.setLoadControl(new androidx.media3.exoplayer.DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(
|
||||
1000,
|
||||
3000,
|
||||
500,
|
||||
1000
|
||||
)
|
||||
.build())
|
||||
.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) {
|
||||
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);
|
||||
if (triedAltUrl || TextUtils.isEmpty(finalComputedAltUrl)) {
|
||||
binding.offlineLayout.setVisibility(View.VISIBLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 自动重试
|
||||
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);
|
||||
triedAltUrl = true;
|
||||
exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl));
|
||||
exo.prepare();
|
||||
exo.setPlayWhenReady(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(int playbackState) {
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
binding.offlineLayout.setVisibility(View.GONE);
|
||||
retryCount = 0;
|
||||
// 只显示一次连接消息
|
||||
if (!hasShownConnectedMessage) {
|
||||
hasShownConnectedMessage = true;
|
||||
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
|
||||
}
|
||||
} else if (playbackState == Player.STATE_BUFFERING) {
|
||||
android.util.Log.d("ExoPlayer", "正在缓冲...");
|
||||
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
android.util.Log.d("ExoPlayer", "开始播放: " + url);
|
||||
exo.setMediaItem(MediaItem.fromUri(url));
|
||||
exo.prepare();
|
||||
exo.setPlayWhenReady(true);
|
||||
|
|
@ -1106,16 +1019,47 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
|
||||
android.util.Log.d("RoomDetail", "开始播放FLV流: " + flvUrl);
|
||||
|
||||
// 直接使用 HLS 播放,避免 IjkPlayer 崩溃问题
|
||||
// HLS 虽然延迟稍高,但稳定性更好
|
||||
String hlsUrl = fallbackHlsUrl;
|
||||
if (TextUtils.isEmpty(hlsUrl)) {
|
||||
hlsUrl = flvUrl.replace(".flv", ".m3u8");
|
||||
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", "使用 HLS 播放: " + hlsUrl);
|
||||
startHls(hlsUrl, null);
|
||||
}
|
||||
|
||||
private void prepareIjk(String url) {
|
||||
|
|
@ -1123,39 +1067,36 @@ 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);
|
||||
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, "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, "reconnect_streamed", 1);
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_delay_max", 5);
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_delay_max", 5); // 最大重连延迟5秒
|
||||
|
||||
p.setOnPreparedListener(mp -> {
|
||||
binding.offlineLayout.setVisibility(View.GONE);
|
||||
mp.start();
|
||||
// 只显示一次连接消息
|
||||
if (!hasShownConnectedMessage) {
|
||||
hasShownConnectedMessage = true;
|
||||
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", 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);
|
||||
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);
|
||||
// 5秒后尝试重新连接
|
||||
handler.postDelayed(() -> {
|
||||
if (!isFinishing() && !isDestroyed() && room != null && room.isLive()) {
|
||||
fetchRoom(); // 重新获取房间信息并播放
|
||||
}
|
||||
}, 5000);
|
||||
return true;
|
||||
}
|
||||
ijkFallbackTried = true;
|
||||
startHls(ijkFallbackHlsUrl, null);
|
||||
|
|
@ -1192,34 +1133,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean ijkLibLoadFailed = false;
|
||||
|
||||
private static void ensureIjkLibsLoaded() {
|
||||
if (ijkLibLoaded || ijkLibLoadFailed) return;
|
||||
if (ijkLibLoaded) 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");
|
||||
ijkLibLoaded = true;
|
||||
android.util.Log.d("IjkPlayer", "IjkPlayer 库加载成功");
|
||||
} catch (Throwable e) {
|
||||
android.util.Log.e("IjkPlayer", "加载IjkPlayer库失败: " + e.getMessage());
|
||||
ijkLibLoadFailed = true;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
ijkLibLoaded = true;
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
|||
|
||||
/**
|
||||
* 连接信令服务器
|
||||
* 通话服务始终使用远程服务器地址
|
||||
*/
|
||||
public void connect(int userId) {
|
||||
Log.d(TAG, "connect() called, userId: " + userId);
|
||||
|
|
@ -89,9 +88,8 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
|||
Log.d(TAG, "已经连接,跳过");
|
||||
return;
|
||||
}
|
||||
// 通话服务使用独立的远程服务器地址
|
||||
String baseUrl = WebRTCConfig.getLiveServerUrl();
|
||||
Log.d(TAG, "连接信令服务器(远程),baseUrl: " + baseUrl);
|
||||
String baseUrl = ApiClient.getCurrentBaseUrl(context);
|
||||
Log.d(TAG, "连接信令服务器,baseUrl: " + baseUrl);
|
||||
signalingClient = new CallSignalingClient(baseUrl, userId);
|
||||
signalingClient.setListener(this);
|
||||
signalingClient.connect();
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
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",
|
||||
"stun:stun.qq.com:3478",
|
||||
"stun:stun.miwifi.com:3478"
|
||||
"stun:stun.l.google.com:19302", // Google STUN(国内可能不稳定)
|
||||
"stun:stun.qq.com:3478", // 腾讯 STUN(国内推荐)
|
||||
"stun:stun.miwifi.com:3478" // 小米 STUN(国内推荐)
|
||||
};
|
||||
|
||||
// ============ TURN 服务器(从BuildConfig读取)============
|
||||
public static final String TURN_SERVER_URL = "turn:" + BuildConfig.TURN_SERVER_HOST + ":" + BuildConfig.TURN_SERVER_PORT;
|
||||
// ============ TURN 服务器 ============
|
||||
// TURN 用于在P2P连接失败时进行中继转发
|
||||
|
||||
// 你的服务器TURN地址
|
||||
public static final String TURN_SERVER_URL = "turn:1.15.149.240:3478";
|
||||
|
||||
// TURN 服务器用户名
|
||||
public static final String TURN_USERNAME = "turnuser";
|
||||
|
|
@ -25,12 +26,27 @@ public class WebRTCConfig {
|
|||
// TURN 服务器密码
|
||||
public static final String TURN_PASSWORD = "TurnPass123456";
|
||||
|
||||
// ============ 直播/通话服务地址 ============
|
||||
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 + "/";
|
||||
}
|
||||
// ============ 使用说明 ============
|
||||
/*
|
||||
* 局域网测试:
|
||||
* - 不需要修改,当前配置即可使用
|
||||
*
|
||||
* 部署到公网服务器:
|
||||
* 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
|
||||
*/
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#FFF8E1" />
|
||||
<corners android:radius="8dp" />
|
||||
<stroke android:width="1dp" android:color="#FFE082" />
|
||||
</shape>
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
-- 好友系统数据库表
|
||||
-- 请在服务器数据库中执行此脚本
|
||||
|
||||
-- 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='私聊会话表';
|
||||
177
环境配置指南.md
177
环境配置指南.md
|
|
@ -1,177 +0,0 @@
|
|||
# 环境配置指南
|
||||
|
||||
本项目采用分离配置架构:
|
||||
- **普通业务功能**(用户、消息、商城等)→ 可切换本地/远程
|
||||
- **直播/通话服务** → 始终连接远程服务器
|
||||
|
||||
---
|
||||
|
||||
## 一、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 | 缓存 |
|
||||
141
环境配置汇总.md
141
环境配置汇总.md
|
|
@ -1,141 +0,0 @@
|
|||
# 环境配置汇总
|
||||
|
||||
## 一、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` 目录到服务器
|
||||
Loading…
Reference in New Issue
Block a user