1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-07-16 21:26:13 +08:00

完善弹幕部分

This commit is contained in:
xiaochong0302 2020-07-10 18:46:57 +08:00
parent 923e311f79
commit 5039b7891d
11 changed files with 408 additions and 95 deletions

View File

@ -0,0 +1,99 @@
<?php
namespace App\Builders;
use App\Repos\Chapter as ChapterRepo;
use App\Repos\Course as CourseRepo;
use App\Repos\User as UserRepo;
class DanmuList extends Builder
{
public function handleCourses(array $danmus)
{
$courses = $this->getCourses($danmus);
foreach ($danmus as $key => $danmu) {
$danmus[$key]['course'] = $courses[$danmu['course_id']] ?? new \stdClass();
}
return $danmus;
}
public function handleChapters(array $danmus)
{
$chapters = $this->getChapters($danmus);
foreach ($danmus as $key => $danmu) {
$danmus[$key]['chapter'] = $chapters[$danmu['chapter_id']] ?? new \stdClass();
}
return $danmus;
}
public function handleUsers(array $danmus)
{
$users = $this->getUsers($danmus);
foreach ($danmus as $key => $danmu) {
$danmus[$key]['user'] = $users[$danmu['user_id']] ?? new \stdClass();
}
return $danmus;
}
public function getCourses(array $danmus)
{
$ids = kg_array_column($danmus, 'course_id');
$courseRepo = new CourseRepo();
$courses = $courseRepo->findByIds($ids, ['id', 'title']);
$result = [];
foreach ($courses->toArray() as $course) {
$result[$course['id']] = $course;
}
return $result;
}
public function getChapters(array $danmus)
{
$ids = kg_array_column($danmus, 'chapter_id');
$chapterRepo = new ChapterRepo();
$chapters = $chapterRepo->findByIds($ids, ['id', 'title']);
$result = [];
foreach ($chapters->toArray() as $chapter) {
$result[$chapter['id']] = $chapter;
}
return $result;
}
public function getUsers(array $danmus)
{
$ids = kg_array_column($danmus, 'user_id');
$userRepo = new UserRepo();
$users = $userRepo->findByIds($ids, ['id', 'name', 'avatar']);
$baseUrl = kg_ci_base_url();
$result = [];
foreach ($users->toArray() as $user) {
$user['avatar'] = $baseUrl . $user['avatar'];
$result[$user['id']] = $user;
}
return $result;
}
}

View File

@ -5,6 +5,7 @@ namespace App\Http\Web\Controllers;
use App\Services\Frontend\Chapter\AgreeVote as ChapterAgreeVoteService; use App\Services\Frontend\Chapter\AgreeVote as ChapterAgreeVoteService;
use App\Services\Frontend\Chapter\ChapterInfo as ChapterInfoService; use App\Services\Frontend\Chapter\ChapterInfo as ChapterInfoService;
use App\Services\Frontend\Chapter\CommentList as ChapterCommentListService; use App\Services\Frontend\Chapter\CommentList as ChapterCommentListService;
use App\Services\Frontend\Chapter\DanmuList as ChapterDanmuListService;
use App\Services\Frontend\Chapter\Learning as ChapterLearningService; use App\Services\Frontend\Chapter\Learning as ChapterLearningService;
use App\Services\Frontend\Chapter\OpposeVote as ChapterOpposeVoteService; use App\Services\Frontend\Chapter\OpposeVote as ChapterOpposeVoteService;
use App\Services\Frontend\Course\ChapterList as CourseChapterListService; use App\Services\Frontend\Course\ChapterList as CourseChapterListService;
@ -53,6 +54,18 @@ class ChapterController extends Controller
$this->view->setVar('chapters', $chapters); $this->view->setVar('chapters', $chapters);
} }
/**
* @Get("/{id:[0-9]+}/danmu", name="web.chapter.danmu")
*/
public function danmuAction($id)
{
$service = new ChapterDanmuListService();
$items = $service->handle($id);
return $this->jsonSuccess(['items' => $items]);
}
/** /**
* @Get("/{id:[0-9]+}/comments", name="web.chapter.comments") * @Get("/{id:[0-9]+}/comments", name="web.chapter.comments")
*/ */

View File

@ -3,6 +3,7 @@
{% block content %} {% block content %}
{% set learning_url = url({'for':'web.chapter.learning','id':chapter.id}) %} {% set learning_url = url({'for':'web.chapter.learning','id':chapter.id}) %}
{% set danmu_url = url({'for':'web.chapter.danmu','id':chapter.id}) %}
<div class="breadcrumb"> <div class="breadcrumb">
<span class="layui-breadcrumb"> <span class="layui-breadcrumb">
@ -20,15 +21,15 @@
<div class="danmu-action container"> <div class="danmu-action container">
<form class="layui-form" action="{{ url({'for':'web.danmu.create'}) }}"> <form class="layui-form" action="{{ url({'for':'web.danmu.create'}) }}">
<div class="layui-input-inline" style="width: 100px;"> <div class="layui-input-inline" style="width: 100px;">
<input type="checkbox" name="status" title="弹幕" checked="checked" lay-filter="status"> <input type="checkbox" name="status" title="弹幕" checked="checked" lay-filter="danmu.status">
</div> </div>
<div class="layui-input-inline" style="width: 655px;"> <div class="layui-input-inline" style="width: 655px;">
{% if auth_user.id > 0 %} {% if auth_user.id > 0 %}
<input class="layui-input" type="text" name="text" maxlength="50" placeholder="快来发个弹幕吧" lay-verType="tips" lay-verify="required"> <input class="layui-input" type="text" name="danmu.text" maxlength="50" placeholder="快来发个弹幕吧" lay-verType="tips" lay-verify="required">
{% else %} {% else %}
<input class="layui-input" type="text" name="text" placeholder="登录后才可以发送弹幕哦" readonly="readonly"> <input class="layui-input" type="text" name="danmu.text" placeholder="登录后才可以发送弹幕哦" readonly="readonly">
{% endif %} {% endif %}
<button class="layui-hide" type="submit" lay-submit="true" lay-filter="chat">发送</button> <button class="layui-hide" type="submit" lay-submit="true" lay-filter="danmu.send">发送</button>
</div> </div>
</form> </form>
</div> </div>
@ -40,8 +41,10 @@
<div class="layui-hide"> <div class="layui-hide">
<input type="hidden" name="chapter.id" value="{{ chapter.id }}"> <input type="hidden" name="chapter.id" value="{{ chapter.id }}">
<input type="hidden" name="chapter.position" value="{{ chapter.me.position }}">
<input type="hidden" name="chapter.plan_id" value="{{ chapter.me.plan_id }}"> <input type="hidden" name="chapter.plan_id" value="{{ chapter.me.plan_id }}">
<input type="hidden" name="chapter.learning_url" value="{{ learning_url }}"> <input type="hidden" name="chapter.learning_url" value="{{ learning_url }}">
<input type="hidden" name="chapter.danmu_url" value="{{ danmu_url }}">
<input type="hidden" name="chapter.play_urls" value='{{ chapter.play_urls|json_encode }}'> <input type="hidden" name="chapter.play_urls" value='{{ chapter.play_urls|json_encode }}'>
</div> </div>

View File

@ -16,9 +16,18 @@ class Danmu extends Model
/** /**
* 位置类型 * 位置类型
*/ */
const POS_MOVE = 0; // 滚动 const POSITION_MOVE = 0; // 滚动
const POS_TOP = 1; // 顶部 const POSITION_TOP = 1; // 顶部
const POS_BOTTOM = 2; // 底部 const POSITION_BOTTOM = 2; // 底部
/**
* 颜色类型
*/
const COLOR_WHITE = 'white'; // 白色
const COLOR_RED = 'red'; // 红色
const COLOR_BLUE = 'blue'; // 蓝色
const COLOR_GREEN = 'green'; // 绿色
const COLOR_YELLOW = 'yellow'; // 黄色
/** /**
* 主键编号 * 主键编号
@ -141,17 +150,51 @@ class Danmu extends Model
public static function sizeTypes() public static function sizeTypes()
{ {
return [ return [
'0' => '小号', self::SIZE_SMALL => '小号',
'1' => '大号', self::SIZE_BIG => '大号',
]; ];
} }
public static function positionTypes() public static function posTypes()
{ {
return [ return [
'0' => '滚动', self::POSITION_MOVE => '滚动',
'1' => '顶部', self::POSITION_TOP => '顶部',
'' => '底部', self::POSITION_BOTTOM => '底部',
]; ];
} }
public static function colorTypes()
{
return [
self::COLOR_WHITE => '白色',
self::COLOR_RED => '红色',
self::COLOR_GREEN => '绿色',
self::COLOR_BLUE => '蓝色',
self::COLOR_YELLOW => '黄色',
];
}
public static function randPos()
{
$types = self::positionTypes();
$keys = array_keys($types);
$index = array_rand($keys);
return $keys[$index];
}
public static function randColor()
{
$types = self::colorTypes();
$keys = array_keys($types);
$index = array_rand($keys);
return $keys[$index];
}
} }

View File

@ -82,4 +82,43 @@ class Danmu extends Repository
->execute(); ->execute();
} }
/**
* @param array $where
* @return ResultsetInterface|Resultset|DanmuModel[]
*/
public function findAll($where = [])
{
$query = DanmuModel::query();
$query->where('1 = 1');
if (!empty($where['course_id'])) {
$query->andWhere('course_id = :course_id:', ['course_id' => $where['course_id']]);
}
if (!empty($where['chapter_id'])) {
$query->andWhere('chapter_id = :chapter_id:', ['chapter_id' => $where['chapter_id']]);
}
if (!empty($where['user_id'])) {
$query->andWhere('user_id = :user_id:', ['user_id' => $where['user_id']]);
}
if (!empty($where['start_time']) && !empty($where['end_time'])) {
$query->betweenWhere('time', $where['start_time'], $where['end_time']);
}
if (isset($where['published'])) {
$query->andWhere('published = :published:', ['published' => $where['published']]);
}
if (isset($where['deleted'])) {
$query->andWhere('deleted = :deleted:', ['deleted' => $where['deleted']]);
}
$query->orderBy('id DESC');
return $query->execute();
}
} }

View File

@ -60,6 +60,7 @@ class ChapterInfo extends FrontendService
$me = [ $me = [
'plan_id' => 0, 'plan_id' => 0,
'position' => 0,
'joined' => 0, 'joined' => 0,
'owned' => 0, 'owned' => 0,
'agreed' => 0, 'agreed' => 0,
@ -70,6 +71,10 @@ class ChapterInfo extends FrontendService
$me['plan_id'] = $this->courseUser->plan_id; $me['plan_id'] = $this->courseUser->plan_id;
} }
if ($this->chapterUser) {
$me['position'] = $this->chapterUser->position;
}
$me['joined'] = $this->joinedChapter ? 1 : 0; $me['joined'] = $this->joinedChapter ? 1 : 0;
$me['owned'] = $this->ownedChapter ? 1 : 0; $me['owned'] = $this->ownedChapter ? 1 : 0;
@ -253,6 +258,16 @@ class ChapterInfo extends FrontendService
$this->incrChapterUserCount($chapter); $this->incrChapterUserCount($chapter);
} }
protected function getVodPosition(ChapterModel $chapter, UserModel $user)
{
}
protected function getLiveStreamName($id)
{
return "chapter_{$id}";
}
protected function incrCourseUserCount(CourseModel $course) protected function incrCourseUserCount(CourseModel $course)
{ {
$this->eventsManager->fire('courseCounter:incrUserCount', $this, $course); $this->eventsManager->fire('courseCounter:incrUserCount', $this, $course);
@ -263,9 +278,4 @@ class ChapterInfo extends FrontendService
$this->eventsManager->fire('chapterCounter:incrUserCount', $this, $chapter); $this->eventsManager->fire('chapterCounter:incrUserCount', $this, $chapter);
} }
protected function getLiveStreamName($id)
{
return "chapter_{$id}";
}
} }

View File

@ -0,0 +1,67 @@
<?php
namespace App\Services\Frontend\Chapter;
use App\Builders\DanmuList as DanmuListBuilder;
use App\Repos\Danmu as DanmuRepo;
use App\Services\Frontend\ChapterTrait;
use App\Services\Frontend\Service as FrontendService;
class DanmuList extends FrontendService
{
use ChapterTrait;
public function handle($id)
{
$chapter = $this->checkChapter($id);
$params = [];
$params['chapter_id'] = $chapter->id;
$params['published'] = 1;
$danmuRepo = new DanmuRepo();
$items = $danmuRepo->findAll($params);
$result = [];
if ($items->count() > 0) {
$result = $this->handleItems($items->toArray());
}
return $result;
}
/**
* @param array $items
* @return array
*/
protected function handleItems($items)
{
$builder = new DanmuListBuilder();
$users = $builder->getUsers($items);
$result = [];
foreach ($items as $item) {
$user = $users[$item['user_id']] ?? new \stdClass();
$result[] = [
'id' => $item['id'],
'text' => $item['text'],
'color' => $item['color'],
'size' => $item['size'],
'time' => $item['time'],
'position' => $item['position'],
'user' => $user,
];
}
return $result;
}
}

View File

@ -38,7 +38,9 @@ class DanmuCreate extends FrontendService
$data['course_id'] = $chapter->course_id; $data['course_id'] = $chapter->course_id;
$data['chapter_id'] = $chapter->id; $data['chapter_id'] = $chapter->id;
$data['user_id'] = $user->id; $data['user_id'] = $user->id;
$data['color'] = 'white'; $data['position'] = DanmuModel::POSITION_MOVE;
$data['color'] = DanmuModel::COLOR_WHITE;
$data['size'] = DanmuModel::SIZE_SMALL;
$data['published'] = 1; $data['published'] = 1;
$danmu->create($data); $danmu->create($data);

View File

@ -17,9 +17,13 @@ class HelpList extends FrontendService
$helps = $helpRepo->findAll($params); $helps = $helpRepo->findAll($params);
$result = [];
if ($helps->count() > 0) { if ($helps->count() > 0) {
return $this->handleHelps($helps); $result = $this->handleHelps($helps);
} }
return $result;
} }
/** /**
@ -31,7 +35,6 @@ class HelpList extends FrontendService
$items = []; $items = [];
foreach ($helps as $help) { foreach ($helps as $help) {
$items[] = [ $items[] = [
'id' => $help->id, 'id' => $help->id,
'title' => $help->title, 'title' => $help->title,

View File

@ -5,7 +5,6 @@ layui.use(['jquery', 'helper'], function () {
var interval = null; var interval = null;
var intervalTime = 15000; var intervalTime = 15000;
var position = 0;
var userId = window.koogua.user.id; var userId = window.koogua.user.id;
var chapterId = $('input[name="chapter.id"]').val(); var chapterId = $('input[name="chapter.id"]').val();
var planId = $('input[name="chapter.plan_id"]').val(); var planId = $('input[name="chapter.plan_id"]').val();
@ -57,24 +56,18 @@ layui.use(['jquery', 'helper'], function () {
options.m3u8_sd = playUrls.m3u8.sd; options.m3u8_sd = playUrls.m3u8.sd;
} }
if (userId !== '0' && planId !== '0') { options.listener = function (msg) {
options.listener = function (msg) { if (msg.type === 'play') {
if (msg.type === 'play') { start();
start(); } else if (msg.type === 'pause') {
} else if (msg.type === 'pause') { stop();
stop(); } else if (msg.type === 'end') {
} else if (msg.type === 'end') { stop();
stop();
}
} }
} };
var player = new TcPlayer('player', options); var player = new TcPlayer('player', options);
if (position > 0) {
player.currentTime(position);
}
function start() { function start() {
if (interval != null) { if (interval != null) {
clearInterval(interval); clearInterval(interval);
@ -84,22 +77,26 @@ layui.use(['jquery', 'helper'], function () {
} }
function stop() { function stop() {
clearInterval(interval); if (interval != null) {
interval = null; clearInterval(interval);
interval = null;
}
} }
function learning() { function learning() {
$.ajax({ if (userId !== '0' && planId !== '0') {
type: 'POST', $.ajax({
url: learningUrl, type: 'POST',
data: { url: learningUrl,
request_id: requestId, data: {
chapter_id: chapterId, plan_id: planId,
plan_id: planId, chapter_id: chapterId,
interval: intervalTime, request_id: requestId,
position: player.currentTime(), interval: intervalTime,
} position: player.currentTime(),
}); }
});
}
} }
}); });

View File

@ -1,18 +1,21 @@
layui.use(['jquery', 'form', 'helper'], function () { layui.use(['jquery', 'form', 'layer', 'helper'], function () {
var $ = layui.jquery; var $ = layui.jquery;
var form = layui.form; var form = layui.form;
var layer = layui.layer;
var helper = layui.helper; var helper = layui.helper;
var interval = null; var interval = null;
var intervalTime = 15000; var intervalTime = 15000;
var position = 0;
var userId = window.koogua.user.id; var userId = window.koogua.user.id;
var requestId = helper.getRequestId();
var chapterId = $('input[name="chapter.id"]').val(); var chapterId = $('input[name="chapter.id"]').val();
var planId = $('input[name="chapter.plan_id"]').val(); var planId = $('input[name="chapter.plan_id"]').val();
var lastPosition = $('input[name="chapter.position"]').val();
var learningUrl = $('input[name="chapter.learning_url"]').val(); var learningUrl = $('input[name="chapter.learning_url"]').val();
var danmuListUrl = $('input[name="chapter.danmu_url"]').val();
var playUrls = JSON.parse($('input[name="chapter.play_urls"]').val()); var playUrls = JSON.parse($('input[name="chapter.play_urls"]').val());
var requestId = helper.getRequestId(); var $danmuText = $('input[name="danmu.text"]');
var options = { var options = {
autoplay: false, autoplay: false,
@ -34,17 +37,22 @@ layui.use(['jquery', 'form', 'helper'], function () {
options.listener = function (msg) { options.listener = function (msg) {
if (msg.type === 'play') { if (msg.type === 'play') {
start(); play();
} else if (msg.type === 'pause') { } else if (msg.type === 'pause') {
stop(); pause();
} else if (msg.type === 'end') { } else if (msg.type === 'ended') {
stop(); ended();
} }
}; };
var player = new TcPlayer('player', options); var player = new TcPlayer('player', options);
if (position > 0) { var position = parseInt(lastPosition);
/**
* 过于接近结束位置当作已结束处理
*/
if (position > 0 && player.duration() - position > 10) {
player.currentTime(position); player.currentTime(position);
} }
@ -55,14 +63,9 @@ layui.use(['jquery', 'form', 'helper'], function () {
height: 380 height: 380
}); });
//再添加三个弹幕 initDanmu();
$("#danmu").danmu("addDanmu", [
{text: "这是滚动弹幕", color: "white", size: 0, position: 0, time: 120}
, {text: "这是顶部弹幕", color: "yellow", size: 0, position: 1, time: 120}
, {text: "这是底部弹幕", color: "red", size: 0, position: 2, time: 120}
]);
form.on('checkbox(status)', function (data) { form.on('checkbox(danmu.status)', function (data) {
if (data.elem.checked) { if (data.elem.checked) {
$('#danmu').danmu('setOpacity', 1); $('#danmu').danmu('setOpacity', 1);
} else { } else {
@ -70,35 +73,65 @@ layui.use(['jquery', 'form', 'helper'], function () {
} }
}); });
form.on('submit(chat)', function (data) { form.on('submit(danmu.send)', function (data) {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: data.form.action, url: data.form.action,
data: { data: {
text: data.field.text, text: $danmuText.val(),
time: player.currentTime(), time: player.currentTime(),
chapter_id: chapterId, chapter_id: chapterId
}, },
success: function (res) { success: function (res) {
showDanmu(res); $('#danmu').danmu('addDanmu', {
text: res.danmu.text,
color: res.danmu.color,
size: res.danmu.size,
time: (res.danmu.time + 1) * 10, //十分之一秒
position: res.danmu.position,
isnew: 1
});
$danmuText.val('');
},
error: function (xhr) {
var res = JSON.parse(xhr.responseText);
layer.msg(res.msg, {icon: 2});
} }
}); });
return false; return false;
}); });
function start() { function clearLearningInterval() {
if (interval != null) { if (interval != null) {
clearInterval(interval); clearInterval(interval);
interval = null; interval = null;
} }
interval = setInterval(learning, intervalTime);
startDanmu();
} }
function stop() { function setLearningInterval() {
clearInterval(interval); interval = setInterval(learning, intervalTime);
interval = null; }
pauseDanmu();
function play() {
startDanmu();
clearLearningInterval();
setLearningInterval();
}
function pause() {
/**
* 视频结束也会触发暂停事件此时弹幕可能尚未结束
* 时间差区分暂停是手动还是结束触发
*/
if (player.currentTime() < player.duration() - 5) {
pauseDanmu();
}
clearLearningInterval();
}
function ended() {
clearLearningInterval();
learning();
} }
function learning() { function learning() {
@ -107,41 +140,45 @@ layui.use(['jquery', 'form', 'helper'], function () {
type: 'POST', type: 'POST',
url: learningUrl, url: learningUrl,
data: { data: {
request_id: requestId,
chapter_id: chapterId,
plan_id: planId, plan_id: planId,
chapter_id: chapterId,
request_id: requestId,
interval: intervalTime, interval: intervalTime,
position: player.currentTime(), position: player.currentTime()
} }
}); });
} }
} }
function startDanmu() { function startDanmu() {
$('#danmu').danmu('danmuStart'); $('#danmu').danmu('danmuResume');
} }
function pauseDanmu() { function pauseDanmu() {
$('#danmu').danmu('danmuPause'); $('#danmu').danmu('danmuPause');
} }
function showDanmu(res) { /**
/* * 一次性获取弹幕待改进为根据时间轴区间获取
$('#danmu').danmu('addDanmu', { */
text: res.danmu.text, function initDanmu() {
color: res.danmu.color, $.ajax({
size: res.danmu.size, type: 'GET',
time: res.danmu.time, url: danmuListUrl,
position: res.danmu.position, success: function (res) {
isnew: 1 var items = [];
layui.each(res.items, function (index, item) {
items.push({
text: item.text,
color: item.color,
size: item.size,
time: (item.time + 1) * 10,
position: item.position
});
});
$('#danmu').danmu('addDanmu', items);
}
}); });
*/
$("#danmu").danmu("addDanmu", [
{text: "这是滚动弹幕", color: "white", size: 0, position: 0, time: 300}
, {text: "这是顶部弹幕", color: "yellow", size: 0, position: 0, time: 300}
, {text: "这是底部弹幕", color: "red", size: 0, position: 0, time: 300}
]);
$('input[name=text]').val('');
} }
}); });