体育赛事直播系统的炫彩弹幕直播间界面,"东莞梦幻网络科技"的源码技术实现方案,包括前端(Vue.js)、后端(ThinkPHP)、安卓(Java)和iOS(Objective-C)的实现代码。
系统架构设计
技术栈 后端: PHP (ThinkPHP 6.x)
数据库: MySQL + Redis
前端: Vue.js 3 + Element Plus
移动端: Android: Java + Retrofit iOS: Objective-C + AFNetworking
实时通信: WebSocket (Swoole)
后端实现 (ThinkPHP)
数据库设计
objectivec
// 直播表
Schema::create('live_streams', function (Blueprint $table) {
$table->id();
$table->string('title'); // 直播标题
$table->integer('sport_type'); // 运动类型
$table->string('home_team'); // 主队
$table->string('away_team'); // 客队
$table->datetime('start_time'); // 开始时间
$table->string('stream_url'); // 直播流地址
$table->integer('viewer_count')->default(0); // 观看人数
$table->integer('status')->default(0); // 状态 0-未开始 1-直播中 2-已结束
$table->timestamps();
});
// 弹幕表
Schema::create('barrages', function (Blueprint $table) {
$table->id();
$table->integer('live_id'); // 直播ID
$table->integer('user_id'); // 用户ID
$table->string('content'); // 弹幕内容
$table->string('color')->default('#FFFFFF'); // 颜色
$table->integer('position')->default(0); // 位置
$table->timestamps();
});
控制器代码
objectivec
<?php
namespace app\controller;
use app\BaseController;
use think\facade\Db;
use think\facade\Request;
class LiveController extends BaseController
{
// 获取直播列表
public function getLiveList()
{
$page = Request::param('page', 1);
$size = Request::param('size', 10);
$list = Db::name('live_streams')
->where('status', '>=', 0)
->order('start_time', 'desc')
->paginate(['page' => $page, 'list_rows' => $size]);
return json([
'code' => 200,
'data' => $list->items(),
'total' => $list->total()
]);
}
// 获取直播详情
public function getLiveDetail($id)
{
$live = Db::name('live_streams')->find($id);
if (!$live) {
return json(['code' => 404, 'msg' => '直播不存在']);
}
return json([
'code' => 200,
'data' => $live
]);
}
// 发送弹幕
public function sendBarrage()
{
$data = Request::only(['live_id', 'content', 'color', 'position']);
$data['user_id'] = Request::middleware('user_id');
$id = Db::name('barrages')->insertGetId($data);
// 通过WebSocket广播弹幕
$this->app->swoole->pushToAll(json_encode([
'type' => 'barrage',
'data' => array_merge($data, ['id' => $id])
]));
return json(['code' => 200, 'msg' => '发送成功']);
}
// 获取弹幕列表
public function getBarrages($live_id)
{
$list = Db::name('barrages')
->where('live_id', $live_id)
->order('create_time', 'desc')
->limit(100)
->select();
return json([
'code' => 200,
'data' => $list
]);
}
}
Web前端实现 (Vue.js)
页面结构
objectivec
<template>
<div class="live-container">
<!-- 顶部导航 -->
<div class="live-header">
<div class="logo">多功能技</div>
<div class="nav-menu">
<span v-for="item in navItems" :key="item">{{ item }}</span>
</div>
<div class="search-box">
<input type="text" placeholder="搜索">
</div>
</div>
<div class="live-main">
<!-- 左侧直播区域 -->
<div class="live-player">
<video-player :src="currentLive.stream_url" @ready="onPlayerReady" />
<!-- 弹幕区域 -->
<div class="barrage-container">
<barrage-item
v-for="barrage in barrages"
:key="barrage.id"
:content="barrage.content"
:color="barrage.color"
:position="barrage.position"
/>
</div>
<!-- 弹幕输入框 -->
<div class="barrage-input">
<input
v-model="barrageText"
placeholder="发个弹幕吧~"
@keyup.enter="sendBarrage"
>
<color-picker v-model="barrageColor" />
<button @click="sendBarrage">发送</button>
</div>
</div>
<!-- 右侧信息区域 -->
<div class="live-sidebar">
<!-- 比赛信息 -->
<div class="match-info">
<h3>{{ currentLive.home_team }} vs {{ currentLive.away_team }}</h3>
<p>{{ formatTime(currentLive.start_time) }}</p>
<button class="follow-btn">关注</button>
</div>
<!-- 赛季信息 -->
<div class="season-info">
<h4>2021赛季</h4>
<ul>
<li v-for="item in seasonInfos" :key="item">{{ item }}</li>
</ul>
</div>
<!-- 主播信息 -->
<div class="anchor-info">
<h4>主播活动</h4>
<p>有机会参加主播活动和视频讲解</p>
<p>完成日期:周六/周日</p>
<button v-if="!isLogin" class="login-btn" @click="showLogin">登录</button>
<button v-else class="start-live-btn" @click="startLive">开播</button>
</div>
<!-- 数据统计 -->
<div class="stats-info">
<h4>数据统计</h4>
<div class="stat-item" v-for="stat in stats" :key="stat.label">
<span>{{ stat.label }}</span>
<span>{{ stat.value }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
Vue组件逻辑
objectivec
<script>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { getLiveDetail, getBarrages, sendBarrage } from '@/api/live'
import { connectWebSocket } from '@/utils/websocket'
export default {
name: 'LivePage',
setup() {
const route = useRoute()
const liveId = route.params.id
// 数据定义
const currentLive = ref({})
const barrages = ref([])
const barrageText = ref('')
const barrageColor = ref('#FFFFFF')
const isLogin = ref(false)
// 静态数据
const navItems = ref(['即时比分', '直播', '赛程', '头条', '社区', '资料库', 'APP下载'])
const seasonInfos = ref([
'东包腿个区,大牌跑出爱!',
'三部曲队,蓝色联赛冠军',
'开通直台'
])
const stats = ref([
{ label: '最大伤害', value: '3421' },
{ label: '最大伤害', value: '3010' },
{ label: '持续伤害', value: '5' },
// 其他统计数据...
])
// 初始化直播数据
const initLiveData = async () => {
const res = await getLiveDetail(liveId)
currentLive.value = res.data
const barrageRes = await getBarrages(liveId)
barrages.value = barrageRes.data
}
// 发送弹幕
const sendBarrage = async () => {
if (!barrageText.value.trim()) return
await sendBarrage({
live_id: liveId,
content: barrageText.value,
color: barrageColor.value,
position: Math.floor(Math.random() * 3) // 0-2随机位置
})
barrageText.value = ''
}
// WebSocket处理
let socket = null
const initWebSocket = () => {
socket = connectWebSocket()
socket.on('barrage', (data) => {
if (data.live_id === liveId) {
barrages.value.unshift(data)
// 限制弹幕数量
if (barrages.value.length > 100) {
barrages.value.pop()
}
}
})
socket.on('viewer_count', (data) => {
if (data.live_id === liveId) {
currentLive.value.viewer_count = data.count
}
})
}
// 生命周期
onMounted(() => {
initLiveData()
initWebSocket()
})
onUnmounted(() => {
if (socket) socket.close()
})
return {
currentLive,
barrages,
barrageText,
barrageColor,
isLogin,
navItems,
seasonInfos,
stats,
sendBarrage
}
}
}
</script>
样式部分
objectivec
<style scoped>
.live-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.live-header {
height: 60px;
background-color: #1a1a1a;
color: white;
display: flex;
align-items: center;
padding: 0 20px;
}
.logo {
font-size: 24px;
font-weight: bold;
margin-right: 40px;
}
.nav-menu {
flex: 1;
display: flex;
gap: 20px;
}
.nav-menu span {
cursor: pointer;
padding: 5px 10px;
}
.nav-menu span:hover {
color: #ffcc00;
}
.search-box input {
padding: 8px 15px;
border-radius: 4px;
border: none;
outline: none;
}
.live-main {
flex: 1;
display: flex;
overflow: hidden;
}
.live-player {
flex: 3;
position: relative;
background-color: #000;
}
.barrage-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 80px;
pointer-events: none;
overflow: hidden;
}
.barrage-input {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
display: flex;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 4px;
padding: 10px;
}
.barrage-input input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: white;
padding: 5px 10px;
}
.live-sidebar {
flex: 1;
background-color: white;
overflow-y: auto;
padding: 20px;
}
.match-info, .season-info, .anchor-info, .stats-info {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.follow-btn, .login-btn, .start-live-btn {
padding: 8px 15px;
background-color: #ff4d4f;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.stat-item {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
</style>
移动端实现
Android实现 (Java)
objectivec
// LiveActivity.java
public class LiveActivity extends AppCompatActivity {
private VideoView videoView;
private RecyclerView barrageRecyclerView;
private EditText barrageEdit;
private Button sendButton;
private LiveViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_live);
// 初始化视图
videoView = findViewById(R.id.video_view);
barrageRecyclerView = findViewById(R.id.barrage_recycler);
barrageEdit = findViewById(R.id.barrage_edit);
sendButton = findViewById(R.id.send_button);
// 设置布局管理器
barrageRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 初始化ViewModel
viewModel = new ViewModelProvider(this).get(LiveViewModel.class);
// 获取直播ID
String liveId = getIntent().getStringExtra("live_id");
// 观察数据变化
viewModel.getLiveData().observe(this, live -> {
// 更新UI
videoView.setVideoURI(Uri.parse(live.getStreamUrl()));
videoView.start();
});
viewModel.getBarrages().observe(this, barrages -> {
// 更新弹幕列表
BarrageAdapter adapter = new BarrageAdapter(barrages);
barrageRecyclerView.setAdapter(adapter);
});
// 发送弹幕
sendButton.setOnClickListener(v -> {
String content = barrageEdit.getText().toString();
if (!TextUtils.isEmpty(content)) {
viewModel.sendBarrage(liveId, content);
barrageEdit.setText("");
}
});
// 加载数据
viewModel.loadLiveData(liveId);
viewModel.connectWebSocket(liveId);
}
@Override
protected void onDestroy() {
super.onDestroy();
viewModel.disconnectWebSocket();
}
}
// LiveViewModel.java
public class LiveViewModel extends ViewModel {
private MutableLiveData<Live> liveData = new MutableLiveData<>();
private MutableLiveData<List<Barrage>> barrages = new MutableLiveData<>();
private WebSocketClient webSocketClient;
public void loadLiveData(String liveId) {
// 调用API获取直播数据
LiveApi.getLiveDetail(liveId, new Callback<Live>() {
@Override
public void onResponse(Call<Live> call, Response<Live> response) {
if (response.isSuccessful()) {
liveData.postValue(response.body());
}
}
@Override
public void onFailure(Call<Live> call, Throwable t) {
// 错误处理
}
});
}
public void connectWebSocket(String liveId) {
// 创建WebSocket连接
webSocketClient = new WebSocketClient(URI.create("ws://your-server/live/ws")) {
@Override
public void onMessage(String message) {
// 处理WebSocket消息
JSONObject json = new JSONObject(message);
if ("barrage".equals(json.getString("type"))) {
Barrage barrage = new Gson().fromJson(json.getJSONObject("data").toString(), Barrage.class);
List<Barrage> current = barrages.getValue();
if (current == null) {
current = new ArrayList<>();
}
current.add(0, barrage);
barrages.postValue(current);
}
}
};
webSocketClient.connect();
}
public void sendBarrage(String liveId, String content) {
// 发送弹幕
JSONObject json = new JSONObject();
try {
json.put("live_id", liveId);
json.put("content", content);
json.put("color", "#FFFFFF");
json.put("position", 0);
if (webSocketClient != null && webSocketClient.isOpen()) {
webSocketClient.send(json.toString());
}
} catch (JSONException e) {
e.printStackTrace();
}
}
public void disconnectWebSocket() {
if (webSocketClient != null) {
webSocketClient.close();
}
}
// Getter方法...
}
iOS实现 (Objective-C)
objectivec
// LiveViewController.h
@interface LiveViewController : UIViewController
@property (nonatomic, strong) NSString *liveId;
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
@property (nonatomic, strong) UITableView *barrageTableView;
@property (nonatomic, strong) UITextField *barrageTextField;
@property (nonatomic, strong) NSMutableArray<Barrage *> *barrages;
@property (nonatomic, strong) SRWebSocket *webSocket;
@end
// LiveViewController.m
@implementation LiveViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化UI
[self setupUI];
// 加载数据
[self loadLiveData];
// 连接WebSocket
[self connectWebSocket];
}
- (void)setupUI {
// 视频播放器
self.player = [AVPlayer playerWithURL:[NSURL URLWithString:@""]];
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.playerLayer.frame = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height * 0.7);
[self.view.layer addSublayer:self.playerLayer];
// 弹幕列表
self.barrageTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, self.view.bounds.size.height * 0.7, self.view.bounds.size.width, self.view.bounds.size.height * 0.2) style:UITableViewStylePlain];
self.barrageTableView.delegate = self;
self.barrageTableView.dataSource = self;
self.barrageTableView.backgroundColor = [UIColor clearColor];
self.barrageTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[self.view addSubview:self.barrageTableView];
// 弹幕输入框
UIView *inputView = [[UIView alloc] initWithFrame:CGRectMake(0, self.view.bounds.size.height * 0.9, self.view.bounds.size.width, 50)];
inputView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.7];
self.barrageTextField = [[UITextField alloc] initWithFrame:CGRectMake(10, 10, self.view.bounds.size.width - 80, 30)];
self.barrageTextField.placeholder = @"发个弹幕吧~";
self.barrageTextField.textColor = [UIColor whiteColor];
self.barrageTextField.backgroundColor = [UIColor clearColor];
UIButton *sendButton = [UIButton buttonWithType:UIButtonTypeSystem];
sendButton.frame = CGRectMake(self.view.bounds.size.width - 70, 10, 60, 30);
[sendButton setTitle:@"发送" forState:UIControlStateNormal];
[sendButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[sendButton addTarget:self action:@selector(sendBarrage) forControlEvents:UIControlEventTouchUpInside];
[inputView addSubview:self.barrageTextField];
[inputView addSubview:sendButton];
[self.view addSubview:inputView];
}
- (void)loadLiveData {
NSString *url = [NSString stringWithFormat:@"http://your-server/api/live/%@", self.liveId];
[[AFHTTPSessionManager manager] GET:url parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSDictionary *data = responseObject[@"data"];
NSString *streamUrl = data[@"stream_url"];
// 播放视频
self.player = [AVPlayer playerWithURL:[NSURL URLWithString:streamUrl]];
[self.player play];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"Error: %@", error);
}];
}
- (void)connectWebSocket {
NSURL *url = [NSURL URLWithString:@"ws://your-server/live/ws"];
self.webSocket = [[SRWebSocket alloc] initWithURL:url];
self.webSocket.delegate = self;
[self.webSocket open];
}
- (void)sendBarrage {
NSString *content = self.barrageTextField.text;
if (content.length == 0) return;
NSDictionary *data = @{
@"live_id": self.liveId,
@"content": content,
@"color": @"#FFFFFF",
@"position": @0
};
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
if (!error) {
[self.webSocket send:jsonData];
self.barrageTextField.text = @"";
}
}
#pragma mark - SRWebSocketDelegate
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:[message dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
if ([dict[@"type"] isEqualToString:@"barrage"]) {
Barrage *barrage = [[Barrage alloc] initWithDictionary:dict[@"data"] error:nil];
[self.barrages insertObject:barrage atIndex:0];
[self.barrageTableView reloadData];
}
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.barrages.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BarrageCell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"BarrageCell"];
cell.backgroundColor = [UIColor clearColor];
cell.textLabel.textColor = [UIColor whiteColor];
}
Barrage *barrage = self.barrages[indexPath.row];
cell.textLabel.text = barrage.content;
return cell;
}
@end
实时通信方案
WebSocket服务端实现 (Swoole)
objectivec
<?php
namespace app\websocket;
use think\swoole\Websocket;
class LiveWebsocket
{
protected $websocket;
// 用户连接
protected $connections = [];
public function __construct(Websocket $websocket)
{
$this->websocket = $websocket;
}
// 连接建立
public function onOpen($ws, $request)
{
$liveId = $request->get['live_id'];
$userId = $request->get['user_id'];
$this->connections[$liveId][$userId] = $ws->fd;
// 更新观看人数
$count = count($this->connections[$liveId] ?? []);
$this->websocket->emit('viewer_count', [
'live_id' => $liveId,
'count' => $count
]);
}
// 收到消息
public function onMessage($ws, $frame)
{
$data = json_decode($frame->data, true);
switch ($data['type']) {
case 'barrage':
// 广播弹幕
$this->broadcastToLive($data['data']['live_id'], [
'type' => 'barrage',
'data' => $data['data']
]);
break;
case 'heartbeat':
// 心跳检测
break;
}
}
// 连接关闭
public function onClose($ws, $fd)
{
foreach ($this->connections as $liveId => $users) {
if (($key = array_search($fd, $users)) !== false) {
unset($this->connections[$liveId][$key]);
// 更新观看人数
$count = count($this->connections[$liveId] ?? []);
$this->websocket->emit('viewer_count', [
'live_id' => $liveId,
'count' => $count
]);
break;
}
}
}
// 广播到指定直播间
protected function broadcastToLive($liveId, $data)
{
if (isset($this->connections[$liveId])) {
foreach ($this->connections[$liveId] as $fd) {
$this->websocket->push($fd, json_encode($data));
}
}
}
}
这个实现方案涵盖了从后端到前端,再到移动端的完整实现,您可以根据实际需求进行调整和扩展。