1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-06-24 20:06:09 +08:00

点播课时

This commit is contained in:
xiaochong0302 2020-06-14 19:56:14 +08:00
parent 9e44dce13c
commit 4665bdda33
25 changed files with 26888 additions and 10 deletions

View File

@ -3,6 +3,7 @@
namespace App\Http\Admin\Controllers;
use App\Traits\Response as ResponseTrait;
use Phalcon\Mvc\View;
/**
* @RoutePrefix("/admin")
@ -13,7 +14,7 @@ class PublicController extends \Phalcon\Mvc\Controller
use ResponseTrait;
/**
* @Route("/auth", name="admin.auth")
* @Get("/auth", name="admin.auth")
*/
public function authAction()
{
@ -25,17 +26,20 @@ class PublicController extends \Phalcon\Mvc\Controller
}
/**
* @Route("/forbidden", name="admin.forbidden")
* @Get("/forbidden", name="admin.forbidden")
*/
public function forbiddenAction()
{
$this->view->setRenderLevel(View::LEVEL_LAYOUT);
$this->view->setLayout('error');
if ($this->request->isAjax()) {
return $this->jsonError(['msg' => '无相关操作权限']);
}
}
/**
* @Route("/ip2region", name="admin.ip2region")
* @Get("/ip2region", name="admin.ip2region")
*/
public function ip2regionAction()
{

View File

@ -137,7 +137,7 @@ class Role extends Service
if (in_array('admin.course.list', $routes)) {
$list[] = 'admin.course.chapters';
$list[] = 'admin.chapter.sections';
$list[] = 'admin.chapter.lessons';
}
if (array_intersect(['admin.course.add', 'admin.course.edit'], $routes)) {

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh-Hans-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>出错啦</title>
{{ icon_link("favicon.ico") }}
{{ css_link("lib/layui/css/layui.css") }}
{{ css_link("web/css/error.css") }}
</head>
<body>
{{ content() }}
</body>
</html>

View File

@ -7,6 +7,7 @@ use App\Services\Frontend\Chapter\ChapterInfo as ChapterInfoService;
use App\Services\Frontend\Chapter\CommentList as ChapterCommentListService;
use App\Services\Frontend\Chapter\Learning as ChapterLearningService;
use App\Services\Frontend\Chapter\OpposeVote as ChapterOpposeVoteService;
use App\Services\Frontend\Course\ChapterList as CourseChapterListService;
/**
* @RoutePrefix("/chapter")
@ -23,7 +24,20 @@ class ChapterController extends Controller
$chapter = $service->handle($id);
$this->view->chapter = $chapter;
$service = new CourseChapterListService();
$chapters = $service->handle($chapter['course']['id']);
if ($chapter['model'] == 'vod') {
$this->view->pick('chapter/show_vod');
} elseif ($chapter['model'] == 'live') {
$this->view->pick('chapter/show_live');
} elseif ($chapter['model'] == 'read') {
$this->view->pick('chapter/show_read');
}
$this->view->setVar('chapter', $chapter);
$this->view->setVar('chapters', $chapters);
}
/**

View File

@ -0,0 +1,23 @@
<div class="layui-card">
<div class="layui-card-header">课程目录</div>
<div class="layui-card-body">
<div class="sidebar-chapter-list">
{% for item in chapters %}
<div class="chapter-title layui-elip">{{ item.title }}</div>
<ul class="sidebar-lesson-list">
{% for lesson in item.children %}
{% set url = url({'for':'web.chapter.show','id':lesson.id}) %}
{% set active = (chapter.id == lesson.id) ? 'active' : 'normal' %}
<li class="lesson-title layui-elip">
{% if lesson.me.owned == 1 %}
<a class="{{ active }}" href="{{ url }}" title="{{ lesson.title|e }}">{{ lesson.title }}</a>
{% else %}
<span class="deny" title="{{ lesson.title|e }}">{{ lesson.title }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endfor %}
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
{% extends 'templates/full.volt' %}
{% block content %}
<div class="layout-main clearfix">
<div class="layout-content"></div>
<div class="layout-sidebar">
{{ partial('chapter/menu') }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,92 @@
{% extends 'templates/full.volt' %}
{% block content %}
{% set course_url = url({'for':'web.course.show','id':chapter.course.id}) %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
<span><i class="layui-icon layui-icon-return"></i> <a href="{{ course_url }}">返回课程主页</a></span>
</span>
</div>
<div class="layout-main clearfix">
<div class="layout-content">
<div id="player" class="container"></div>
<div class="comment-list container"></div>
</div>
<div class="layout-sidebar">
{{ partial('chapter/menu') }}
</div>
</div>
{% endblock %}
{% block inline_js %}
<script src="//imgcache.qq.com/open/qcloud/video/vcplayer/TcPlayer-2.3.2.js"></script>
<script>
var interval = null;
var intervalTime = 5000;
var requestId = getRequestId();
var chapterId = '{{ chapter.id }}';
var playUrl = 'https://1255691183.vod2.myqcloud.com/81258db0vodtransgzp1255691183/89b3d8955285890796532522693/v.f220.m3u8?t=5ee5c6ed&exper=0&us=697028&sign=2b7fd89eff92236184eadbaa14a895dd';
var position = 0;
var player = new TcPlayer('player', {
m3u8: playUrl,
autoplay: true,
width: 760,
height: 450,
listener: function (msg) {
if (msg.type === 'play') {
start();
} else if (msg.type === 'pause') {
stop();
} else if (msg.type === 'end') {
stop();
}
}
});
if (position > 0) {
player.currentTime(position);
}
function start() {
if (interval != null) {
clearInterval(interval);
interval = null;
}
interval = setInterval(learning, intervalTime);
}
function stop() {
clearInterval(interval);
interval = null;
}
function learning() {
$.ajax({
type: 'GET',
url: '/admin/vod/learning',
data: {
request_id: requestId,
chapter_id: chapterId,
interval: intervalTime,
position: player.currentTime(),
}
});
}
function getRequestId() {
var id = Date.now().toString(36);
id += Math.random().toString(36).substr(3);
return id;
}
</script>
{% endblock %}

View File

@ -1,6 +1,7 @@
{%- macro vod_lesson_info(lesson) %}
{% set url = lesson.me.owned ? url({'for':'web.chapter.show','id':lesson.id}) : 'javascript:' %}
<a href="{{ url }}">
{% set priv = lesson.me.owned ? 'allow' : 'deny' %}
<a class="{{ priv }}" href="{{ url }}">
<i class="layui-icon layui-icon-play"></i>
<span class="title">{{ lesson.title }}</span>
{% if lesson.free == 1 %}
@ -15,8 +16,9 @@
{%- macro live_lesson_info(lesson) %}
{% set url = lesson.me.owned ? url({'for':'web.chapter.show','id':lesson.id}) : 'javascript:' %}
{% set priv = lesson.me.owned ? 'allow' : 'deny' %}
{% set over_flag = lesson.attrs.end_time < time() ? '已结束' : '' %}
<a href="{{ url }}">
<a class="{{ priv }}" href="{{ url }}">
<i class="layui-icon layui-icon-video"></i>
<span class="title">{{ lesson.title }}</span>
{% if lesson.free == 1 %}
@ -31,7 +33,8 @@
{%- macro read_lesson_info(lesson) %}
{% set url = lesson.me.owned ? url({'for':'web.chapter.show','id':lesson.id}) : 'javascript:' %}
<a href="{{ url }}">
{% set priv = lesson.me.owned ? 'allow' : 'deny' %}
<a class="{{ priv }}" href="{{ url }}">
<i class="layui-icon layui-icon-read"></i>
<span class="title">{{ lesson.title|e }}</span>
{% if lesson.free == 1 %}

View File

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="zh-CN-Hans">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="keywords" content="{{ site_seo.getKeywords() }}">
<meta name="description" content="{{ site_seo.getDescription() }}">
<meta name="csrf-token" content="{{ csrfToken.getToken() }}">
<title>{{ site_seo.getTitle() }}</title>
{{ icon_link('favicon.ico') }}
{{ css_link('lib/layui/css/layui.css') }}
{{ css_link('web/css/common.css') }}
{% block link_css %}{% endblock %}
{% block inline_css %}{% endblock %}
</head>
{% set course_url = url({'for':'web.course.show','id':chapter.course.id}) %}
<body class="chapter-bg">
<div class="chapter-main">
<div class="header clearfix">
<div class="back fl">
<span><i class="layui-icon layui-icon-return"></i> <a href="{{ course_url }}">返回课程主页</a></span>
</div>
<div class="stats fr">
<span class="user">203</span>
<span class="agree">300</span>
<span class="oppose">50</span>
</div>
<div class="action fr">
<span class="user">203</span>
<span class="agree">300</span>
<span class="oppose">50</span>
</div>
</div>
<div class="content">
{% block content %}{% endblock %}
</div>
<div class="chapter-sidebar-btn">
<i class="switch-icon layui-icon layui-icon-shrink-right"></i>
</div>
</div>
<div class="chapter-sidebar">
<div class="layui-tab layui-tab-brief">
<ul class="layui-tab-title">
<li class="layui-this">目录</li>
<li>讨论</li>
<li>资料</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show" id="tab-courses" data-url="#"></div>
<div class="layui-tab-item" id="tab-favorites" data-url="#"></div>
<div class="layui-tab-item" id="tab-friends" data-url="#"></div>
</div>
</div>
</div>
{{ js_include('lib/layui/layui.all.js') }}
{{ js_include('web/js/common.js') }}
<script>
var $ = layui.jquery;
$('.chapter-sidebar-btn').on('click', function () {
var switchIcon = $(this).children('.switch-icon');
var mainBlock = $('.chapter-main');
var sidebarBlock = $('.chapter-sidebar');
var spreadLeft = 'layui-icon-spread-left';
var shrinkRight = 'layui-icon-shrink-right';
if (switchIcon.hasClass(spreadLeft)) {
switchIcon.removeClass(spreadLeft).addClass(shrinkRight);
mainBlock.css('right', 0);
sidebarBlock.css('width', 0);
} else {
switchIcon.removeClass(shrinkRight).addClass(spreadLeft);
mainBlock.css('right', 320);
sidebarBlock.css('width', 320);
}
});
</script>
{% block include_js %}{% endblock %}
{% block inline_js %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,136 @@
<?php
namespace App\Services\Frontend\Chapter;
use App\Models\Chapter as ChapterModel;
use App\Models\Course as CourseModel;
use App\Repos\Chapter as ChapterRepo;
use App\Services\ChapterVod as ChapterVodService;
use App\Services\Frontend\ChapterTrait;
use App\Services\Frontend\Service as FrontendService;
use App\Services\Live as LiveService;
use WhichBrowser\Parser as BrowserParser;
class ChapterBasic extends FrontendService
{
use ChapterTrait;
public function handle($id)
{
$chapter = $this->checkChapterCache($id);
return $this->handleChapter($chapter);
}
protected function handleChapter(ChapterModel $chapter)
{
/**
* @var array $attrs
*/
$attrs = $chapter->attrs;
$result = [];
switch ($attrs['model']) {
case CourseModel::MODEL_VOD:
$result = $this->formatChapterVod($chapter);
break;
case CourseModel::MODEL_LIVE:
$result = $this->formatChapterLive($chapter);
break;
case CourseModel::MODEL_READ:
$result = $this->formatChapterRead($chapter);
break;
}
return $result;
}
protected function formatChapterVod(ChapterModel $chapter)
{
$chapterVodService = new ChapterVodService();
$playUrls = $chapterVodService->getPlayUrls($chapter->id);
/**
* @var array $attrs
*/
$attrs = $chapter->attrs;
return [
'id' => $chapter->id,
'title' => $chapter->title,
'summary' => $chapter->summary,
'model' => $attrs['model'],
'play_urls' => $playUrls,
'user_count' => $chapter->user_count,
'agree_count' => $chapter->agree_count,
'oppose_count' => $chapter->oppose_count,
'comment_count' => $chapter->comment_count,
];
}
protected function formatChapterLive(ChapterModel $chapter)
{
$headers = getallheaders();
$browserParser = new BrowserParser($headers);
$liveService = new LiveService();
$stream = "chapter-{$chapter->id}";
$format = $browserParser->isType('desktop') ? 'flv' : 'hls';
$playUrls = $liveService->getPullUrls($stream, $format);
$chapterRepo = new ChapterRepo();
$live = $chapterRepo->findChapterLive($chapter->id);
/**
* @var array $attrs
*/
$attrs = $chapter->attrs;
return [
'id' => $chapter->id,
'title' => $chapter->title,
'summary' => $chapter->summary,
'model' => $attrs['model'],
'play_urls' => $playUrls,
'start_time' => $live->start_time,
'end_time' => $live->end_time,
'user_count' => $chapter->user_count,
'agree_count' => $chapter->agree_count,
'oppose_count' => $chapter->oppose_count,
'comment_count' => $chapter->comment_count,
];
}
protected function formatChapterRead(ChapterModel $chapter)
{
$chapterRepo = new ChapterRepo();
$read = $chapterRepo->findChapterRead($chapter->id);
/**
* @var array $attrs
*/
$attrs = $chapter->attrs;
return [
'id' => $chapter->id,
'title' => $chapter->title,
'summary' => $chapter->summary,
'model' => $attrs['model'],
'content' => $read->content,
'user_count' => $chapter->user_count,
'agree_count' => $chapter->agree_count,
'oppose_count' => $chapter->oppose_count,
'comment_count' => $chapter->comment_count,
];
}
}

View File

@ -57,6 +57,8 @@ class ChapterInfo extends FrontendService
{
$result = $this->formatChapter($chapter);
$result['course'] = $this->handleCourse($this->course);
$me = [
'agreed' => 0,
'opposed' => 0,
@ -81,6 +83,21 @@ class ChapterInfo extends FrontendService
return $result;
}
protected function handleCourse(CourseModel $course)
{
return [
'id' => $course->id,
'title' => $course->title,
'cover' => $course->cover,
'market_price' => $course->market_price,
'vip_price' => $course->vip_price,
'model' => $course->model,
'level' => $course->level,
'user_count' => $course->user_count,
'lesson_count' => $course->lesson_count,
];
}
protected function formatChapter(ChapterModel $chapter)
{
$item = [];
@ -102,14 +119,20 @@ class ChapterInfo extends FrontendService
protected function formatChapterVod(ChapterModel $chapter)
{
$chapterVodService = new ChapterVodService();
$service = new ChapterVodService();
$playUrls = $chapterVodService->getPlayUrls($chapter->id);
$playUrls = $service->getPlayUrls($chapter->id);
/**
* @var array $attrs
*/
$attrs = $chapter->attrs;
return [
'id' => $chapter->id,
'title' => $chapter->title,
'summary' => $chapter->summary,
'model' => $attrs['model'],
'play_urls' => $playUrls,
'user_count' => $chapter->user_count,
'agree_count' => $chapter->agree_count,
@ -136,10 +159,16 @@ class ChapterInfo extends FrontendService
$live = $chapterRepo->findChapterLive($chapter->id);
/**
* @var array $attrs
*/
$attrs = $chapter->attrs;
return [
'id' => $chapter->id,
'title' => $chapter->title,
'summary' => $chapter->summary,
'model' => $attrs['model'],
'play_urls' => $playUrls,
'start_time' => $live->start_time,
'end_time' => $live->end_time,
@ -156,10 +185,16 @@ class ChapterInfo extends FrontendService
$read = $chapterRepo->findChapterRead($chapter->id);
/**
* @var array $attrs
*/
$attrs = $chapter->attrs;
return [
'id' => $chapter->id,
'title' => $chapter->title,
'summary' => $chapter->summary,
'model' => $attrs['model'],
'content' => $read->content,
'user_count' => $chapter->user_count,
'agree_count' => $chapter->agree_count,

View File

@ -0,0 +1,48 @@
<?php
namespace App\Services\Frontend\Course;
use App\Models\Course as CourseModel;
use App\Services\Frontend\CourseTrait;
use App\Services\Frontend\Service as FrontendService;
class CourseBasic extends FrontendService
{
use CourseTrait;
public function handle($id)
{
$course = $this->checkCourseCache($id);
return $this->handleCourse($course);
}
protected function handleCourse(CourseModel $course)
{
return [
'id' => $course->id,
'title' => $course->title,
'cover' => $course->cover,
'summary' => $course->summary,
'details' => $course->details,
'keywords' => $course->keywords,
'market_price' => $course->market_price,
'vip_price' => $course->vip_price,
'study_expiry' => $course->study_expiry,
'refund_expiry' => $course->refund_expiry,
'rating' => $course->rating,
'model' => $course->model,
'level' => $course->level,
'attrs' => $course->attrs,
'user_count' => $course->user_count,
'lesson_count' => $course->lesson_count,
'package_count' => $course->package_count,
'review_count' => $course->review_count,
'comment_count' => $course->comment_count,
'consult_count' => $course->consult_count,
'favorite_count' => $course->favorite_count,
];
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<cross-domain-policy>
<allow-access-from domain="*"/>
</cross-domain-policy>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,28 @@
Copyright (c) 2017 Dailymotion (http://www.dailymotion.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
src/remux/mp4-generator.js and src/demux/exp-golomb.js implementation in this project
are derived from the HLS library for video.js (https://github.com/videojs/videojs-contrib-hls)
That work is also covered by the Apache 2 License, following copyright:
Copyright (c) 2013-2015 Brightcove
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<language>
<adCountdown>[$second]</adCountdown><!--广告播放结束倒计时-->
<skipDelay>[$second]</skipDelay>
<buttonOver>
<play>点击播放</play>
<pause>暂停播放</pause>
<mute>静音</mute>
<escMute>恢复音量</escMute>
<full>全屏</full>
<escFull>退出全屏</escFull>
<previousPage>上一集</previousPage>
<nextPage>下一集</nextPage>
<definition>点击选择清晰度</definition>
<subtitle>选择字幕</subtitle>
</buttonOver>
<volumeSliderOver>
音量:[$volume]%
</volumeSliderOver>
<buffer>[$percentage]%</buffer>
<timeSliderOver><!--鼠标经过进度条显示的时间格式-->
[$timeh]:[$timei]:[$times]
</timeSliderOver>
<liveAndVod>
[$timeh]:[$timei]:[$times]
</liveAndVod>
<live>
直播中 [$liveTimeY]-[$liveTimem]-[$liveTimed] [$liveTimeh]:[$liveTimei]:[$liveTimes]
</live>
<m3u8Definition>
<name>流畅</name>
<name>低清</name>
<name>标清</name>
<name>高清</name>
<name>超清</name>
<name>蓝光</name>
<name>未知</name>
</m3u8Definition>
<error>
<cannotFindUrl>视频地址不存在</cannotFindUrl>
<streamNotFound>加载失败</streamNotFound>
<formatError>视频格式错误</formatError>
</error>
<definition>自动</definition>
<subtitle>默认</subtitle>
</language>

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,25 @@
::-webkit-scrollbar-button {
height: 0;
width: 0;
display: none;
}
.sidebar-chapter-list::-webkit-scrollbar {
background-color: #fff;
width: 5px;
height: 15px;
}
.sidebar-chapter-list::-webkit-scrollbar-thumb {
border-radius: 5px;
background: #393D49;
}
.sidebar-chapter-list::-webkit-scrollbar-track {
border: 0;
background-color: #fff;
}
.body {
background-color: #f2f2f2;
}
@ -414,6 +436,11 @@
display: block;
}
.lesson-item .deny {
cursor: default;
color: gray;
}
.lesson-item .title {
margin: 0 5px;
}
@ -672,6 +699,97 @@
margin-right: 10px;
}
.sidebar-chapter-list {
max-height: 500px;
overflow-y: auto;
}
.sidebar-chapter-list .chapter-title {
padding-top: 10px;
margin-bottom: 10px;
color: #333;
background-color: #fff;
}
.sidebar-lesson-list {
margin-left: 15px;
}
.sidebar-lesson-list .lesson-title {
line-height: 30px;
font-size: 12px;
}
.lesson-title .active {
color: red;
}
.lesson-title .deny {
color: gray;
}
.chapter-bg {
background-color: #f2f2f2;
}
.chapter-main {
top: 0;
left: 0;
right: 0;
bottom: 0;
position: absolute;
}
.chapter-main .header {
height: 50px;
line-height: 50px;
color: rgba(255, 255, 255, 0.7);
background-color: #393D49;
}
.chapter-main .header a {
color: rgba(255, 255, 255, 0.7);
}
.chapter-main .header .back {
float: left;
margin-left: 5px;
}
.chapter-main .header .stats {
float: left;
margin-left: 50px;
margin-right: 150px;
}
.chapter-main .header .action {
float: right;
margin-right: 150px;
}
.chapter-main .content {
}
.chapter-sidebar {
top: 0;
right: 0;
bottom: 0;
width: 0;
position: absolute;
background-color: white;
}
.chapter-sidebar-btn {
top: 15px;
right: 15px;
position: absolute;
cursor: pointer;
}
.chapter-sidebar-btn .switch-icon {
color: rgba(255, 255, 255, 0.7);
}
.vip-header {
font-size: 18px;
text-align: center;