Commit edac7bc6 authored by kingweight's avatar kingweight

更新视频上传组件

parent a97b98b7
...@@ -3858,6 +3858,11 @@ ...@@ -3858,6 +3858,11 @@
"randomfill": "^1.0.4" "randomfill": "^1.0.4"
} }
}, },
"crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"css": { "css": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
......
...@@ -36,7 +36,8 @@ ...@@ -36,7 +36,8 @@
"spark-md5": "^3.0.0", "spark-md5": "^3.0.0",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"zone.js": "~0.10.2", "zone.js": "~0.10.2",
"querystring": "^0.2.0" "querystring": "^0.2.0",
"crypto-js": "^4.1.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~0.900.3", "@angular-devkit/build-angular": "~0.900.3",
......
...@@ -25,7 +25,7 @@ export class UploadImageWithPreviewComponent implements OnDestroy, OnChanges { ...@@ -25,7 +25,7 @@ export class UploadImageWithPreviewComponent implements OnDestroy, OnChanges {
@Input() @Input()
iconSize = 2; iconSize = 2;
@Input() @Input()
TIP = '点击上传图片'; TIP = '点击上传视频封面';
@Input() @Input()
disableUpload = false; disableUpload = false;
......
<div class="p-video-box"> <div class="p-video-box">
<div class="up-video" style="display: flex;"> <div class="up-video" style="display: flex;">
<!--<nz-upload class="" [nzDisabled]="!showUploadBtn"--> <nz-upload class="p-image-uploader" [nzDisabled]="uploading" [nzShowUploadList]="false" nzAccept=".mp4"
<!--[nzShowUploadList]="false"--> (nzChange)="handleChange($event)" [nzCustomRequest]="customReq"
<!--[nzBeforeUpload]="beforeUpload"-->
<!--nzAccept = ".mp4"-->
<!--style="margin-right: 1rem">-->
<nz-upload class="p-image-uploader" [nzDisabled]="uploading"
[nzShowUploadList]="false"
nzAccept = ".mp4"
[nzAction]="uploadUrl"
[nzData]="uploadData"
(nzChange)="handleChange($event)"
style="margin-right: 1rem"> style="margin-right: 1rem">
<button type="button" nz-button nzType="default" *ngIf="showUploadBtn" [disabled]="uploading" <button type="button" nz-button nzType="default" *ngIf="showUploadBtn" [disabled]="uploading"
[nzLoading]="uploading" > [nzLoading]="uploading">
<i nz-icon nzType="plus" nzTheme="outline"></i> <i nz-icon nzType="plus" nzTheme="outline"></i>
<span>{{ uploading ? 'Uploading' : 'Select Video' }}</span> <span>{{ uploading ? 'Uploading' : 'Select Video' }}</span>
<!--<span>Select Video</span>-->
</button> </button>
</nz-upload> </nz-upload>
<!--<button nz-button nzType="primary" *ngIf="showUploadBtn"--> <button type="button" nz-button nzType="default" *ngIf="showCapture && !uploading" [disabled]="uploading"
<!--style="margin-right: 1rem"--> (click)="captureVideo()"
<!--type="button"--> [nzLoading]="uploading">
<!--[nzLoading]="uploading"--> <i nz-icon nzType="camera" nzTheme="outline"></i>
<!--(click)="handleUpload()"--> <span>截图</span>
<!--[disabled]="!videoFile || uploading">--> </button>
<!--<i nz-icon type="upload" theme="outline"></i>-->
<!--<span>{{ uploading ? 'Uploading' : 'Start Upload' }}</span>-->
<!--</button>-->
<!--
<button type="button"
(click)="extraCover()"
nz-button nzType="default"
[disabled]=" !(videoUrl && videoUrl.constructor.name === 'String')">设置封面</button>
-->
<!--
<nz-divider></nz-divider>
<div [style.display]="!uploading?'none':''" style="position:relative">
<div style="width: calc( 100% - 20px);">
<nz-progress [nzPercent]="progress">
</nz-progress>
</div>
<i (click)="cancelUpload()" class="anticon anticon-close-circle" style="position: absolute;top: 50%;right: 0;transform: translateY(-50%);cursor: pointer"></i>
</div> </div>
--> <div class="p-box d-flex align-items-center p-video-uploader">
</div>
<div class="p-box d-flex align-items-center p-video-uploader">
<div class="p-upload-icon" *ngIf="!showUploadBtn && !videoUrl && !uploading"> <div class="p-upload-icon" *ngIf="!showUploadBtn && !videoUrl && !uploading">
<i nz-icon nzType="upload" nzTheme="outline"></i> <i nz-icon nzType="upload" nzTheme="outline"></i>
<div class="m-3"></div> <div class="m-3"></div>
...@@ -65,8 +26,7 @@ ...@@ -65,8 +26,7 @@
<div class="p-progress-value">{{progress}}%</div> <div class="p-progress-value">{{progress}}%</div>
</div> </div>
</div> </div>
<div class="p-upload-progress-bg dddd " *ngIf="uploading" <div class="p-upload-progress-bg" *ngIf="uploading" [ngClass]="{'smart-bar': showUploadBtn}">
[ngClass]="{'smart-bar': showUploadBtn}" >
<div class="i-bg" [style.width]="progress+'%'"></div> <div class="i-bg" [style.width]="progress+'%'"></div>
<div class="i-text"> <div class="i-text">
<fa-icon icon="cloud-upload-alt"></fa-icon> <fa-icon icon="cloud-upload-alt"></fa-icon>
...@@ -80,15 +40,12 @@ ...@@ -80,15 +40,12 @@
<i nz-icon nzType="loading" nzTheme="outline"></i>Checking... <i nz-icon nzType="loading" nzTheme="outline"></i>Checking...
</div> </div>
</div> </div>
<div class="p-preview" *ngIf="!uploading && videoUrl " > <div class="p-preview" *ngIf="!uploading&&playVedioUrl">
<!--<video crossorigin="anonymous" [src]="videoUrl" controls #videoNode></video>--> <video crossorigin="anonymous" [src]="playVedioUrl" controls #videoNode></video>
<video [src]="safeVideoUrl(videoUrl)" controls #videoNode></video> <!-- <video crossorigin="anonymous" [src]="safeVideoUrl()" controls #videoNode></video> -->
</div> </div>
</div> </div>
<div [style.display]="!checkVideoExists?'none':''"> <div [style.display]="!checkVideoExists?'none':''">
<span><i nz-icon nzType="loading" nzTheme="outline"></i> checking file to upload</span> <span><i nz-icon nzType="loading" nzTheme="outline"></i> checking file to upload</span>
</div>
</div> </div>
</div>
import {Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, SecurityContext, ViewChild} from '@angular/core'; import {Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SecurityContext, ViewChild} from '@angular/core';
import {NzMessageService, UploadChangeParam, UploadFile, UploadFileStatus} from 'ng-zorro-antd';
import {HttpClientModule} from '@angular/common/http';
import {HttpClient, HttpHeaders, HttpRequest, HttpEvent, HttpEventType, HttpResponse, HttpParams} from "@angular/common/http";
import {NzMessageService, UploadChangeParam, UploadFile, UploadFileStatus, UploadXHRArgs} from 'ng-zorro-antd';
import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser';
import cryptoJs from 'crypto-js';
import {v4} from 'uuid';
function httpHeadCall(requsetUrl, callback) {
let xhr = new XMLHttpRequest();
try {
xhr.onreadystatechange = () => {
try {
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 400)) {
callback(true);
} else {
callback(false);
}
}
} catch (e) {
callback(false);
}
};
xhr.open("HEAD", requsetUrl, true);
xhr.send();
xhr.timeout = 15000;
xhr.onerror = (e) => {
callback(false);
};
xhr.ontimeout = (e) => {
callback(false);
};
} catch (e) {
callback(false);
}
}
@Component({ @Component({
...@@ -8,7 +44,7 @@ import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; ...@@ -8,7 +44,7 @@ import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser';
templateUrl: './upload-video.component.html', templateUrl: './upload-video.component.html',
styleUrls: ['./upload-video.component.scss'] styleUrls: ['./upload-video.component.scss']
}) })
export class UploadVideoComponent implements OnChanges, OnDestroy { export class UploadVideoComponent implements OnChanges, OnDestroy, OnInit {
uploading = false; uploading = false;
checking = false; checking = false;
checkVideoExists = false; checkVideoExists = false;
...@@ -24,8 +60,7 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -24,8 +60,7 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
@Input() @Input()
videoUrl = ''; videoUrl = '';
@ViewChild('videoNode', {static: true }) @ViewChild('videoNode', {static: false }) videoNode: ElementRef;
videoNode: ElementRef;
...@@ -38,11 +73,17 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -38,11 +73,17 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
@Output() @Output()
videoUploadFailure = new EventEmitter(); videoUploadFailure = new EventEmitter();
@Output() @Output()
onCapture = new EventEmitter();
@Output()
extractVideoCover = new EventEmitter(); extractVideoCover = new EventEmitter();
@Output()
parentRef= new EventEmitter();
@Input() @Input()
showUploadBtn = true; showUploadBtn = true;
@Input()
showCapture = true;
@Input() @Input()
item: any; item: any;
// videoItem = null; // videoItem = null;
...@@ -51,7 +92,8 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -51,7 +92,8 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
uploadData; uploadData;
constructor(private nzMessageService: NzMessageService, constructor(private nzMessageService: NzMessageService,
private sanitization: DomSanitizer private sanitization: DomSanitizer,
private http:HttpClient
// private cwService: _coursewareService, // private cwService: _coursewareService,
// private resService: ResourceService // private resService: ResourceService
) { ) {
...@@ -59,16 +101,28 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -59,16 +101,28 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
this.videoFile = null; this.videoFile = null;
this.uploadUrl = (<any> window).courseware.uploadUrl(); this.uploadUrl = (<any> window).courseware.uploadVideoUrl();
this.uploadData = (<any> window).courseware.uploadData(); this.uploadData = (<any> window).courseware.uploadData();
window['air'].getUploadCallback = (url, data) => {
window['air'].getUploadVideoCallback = (url, data) => {
this.uploadUrl = url; this.uploadUrl = url;
this.uploadData = data; this.uploadData = data;
}; };
} }
ngOnInit() {
this.playVedioUrl = this.videoUrl.replace(`_h.mp4`, `_l.mp4`);;
this.parentRef.emit(this);
}
md5Short(str, _salt) {
return cryptoJs.MD5(str + _salt).toString();
}
ngOnChanges() { ngOnChanges() {
// if (!this.videoFile || this.showUploadBtn) { // if (!this.videoFile || this.showUploadBtn) {
// return; // return;
...@@ -80,57 +134,132 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -80,57 +134,132 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
URL.revokeObjectURL(this.videoUrl); URL.revokeObjectURL(this.videoUrl);
} }
safeVideoUrl(url) { playVedioUrl = '';
console.log(url); safeVideoUrl() {
return this.sanitization.bypassSecurityTrustResourceUrl(url); // `${url}`; const url = this.videoUrl.replace(`_h.mp4`, `.mp4`).replace(`.mp4`, `_l.mp4`);
console.log(`=============safeVideoUrl==============`,url);
httpHeadCall(url, (flag)=>{
if (flag) {
this.playVedioUrl = url;
} else {
setTimeout(() => {
this.safeVideoUrl();
}, 500);
}
});
return this.playVedioUrl;
} }
videoLoadedMetaData() { videoLoadedMetaData() {
} }
// fileToBlob(file) {
// let reader = new FileReader();
// reader.addEventListener('load', (e) => {
// let base64 = e.target.result;
// let blob = new Blob([base64], { type: file.type });
// console.log("Blob对象", blob);
// })
// reader.readAsDataURL(file)
// }
handleChange(info: UploadChangeParam/* { type: string, file: UploadFile, event: any }*/): void {
console.log('info:' , info);
customReq(item: UploadXHRArgs) {
// md5值计算
const protocolStr = document.location.protocol;
const baseCDN = `${protocolStr}//teach.cdn.ireadabc.com`;
const baseAPI = `${protocolStr}//openapi.iteachabc.com`;
const fileName = v4().replace(/-/g, "");
httpHeadCall(`${baseCDN}/${fileName}_h.mp4`, (flag)=>{
if (flag) {
item.onSuccess!({url: `${baseCDN}/${fileName}.mp4`}, item.file!, event);
} else {
this.http.get(`${baseAPI}/oss/timestamp`, {}).subscribe((tmps: string) => {
const sign = cryptoJs.MD5(tmps+"iplayABC2019").toString();
const params = new HttpParams().set('sign', sign).set('stmp', tmps);
this.http.get(`${baseAPI}/oss/signature`, {params}).subscribe((res) => {
const serve = res['data'];
const formData = new FormData();
formData.append('key', fileName+`.mp4`);
formData.append('bucket', `iplayabc-worker-queue-videolmh`);
formData.append('OSSAccessKeyId', serve.OSSAccessKeyId);
formData.append('policy', serve.policy);
formData.append('Signature', serve.signature);
formData.append('success_action_status', '200');
formData.append('file', item.file as any);
//设置cors跨域请求
const headerstemp = new HttpHeaders({ 'Access-Control-Allow-Origin': '*' });
item.action = `${protocolStr}//iplayabc-worker-queue-videolmh.oss-cn-beijing.aliyuncs.com`;
const req = new HttpRequest('POST', item.action!, formData, {
headers: headerstemp,
reportProgress: true, //进度条
// withCredentials: true
});
// Always returns a `Subscription` object. nz-upload would automatically unsubscribe it at correct time.
return this.http.request(req).subscribe(
(event: HttpEvent<{}>) => {
if (event.type === HttpEventType.UploadProgress) {
if (event.total! > 0) {
// tslint:disable-next-line:no-any
(event as any).percent = (event.loaded / event.total!) * 100;
}
item.onProgress!(event, item.file!);
} else if (event instanceof HttpResponse) {
item.onSuccess!({url: `${baseCDN}/${fileName}.mp4`}, item.file!, event);
}
},
err => {
console.log(err);
item.onError!(err, item.file!);
}
);
});
});
}
});
};
handleChange(info: UploadChangeParam/* { type: string, file: UploadFile, event: any }*/): void {
console.log(info);
switch (info.type) { switch (info.type) {
case 'start': case 'start':
// this.beforeUpload(info.file);
if (!this.checkSelectFile(info.file)) { if (!this.checkSelectFile(info.file)) {
return; return;
} }
this.uploading = true; this.uploading = true;
this.progress = 0; this.progress = 0;
break; break;
case 'success': case 'success':
this.uploadSuccess(info.file); this.uploadSuccess(info.file);
// this.beforeUpload(info.file);
// this.uploadSuccess(info.file);
break; break;
case 'progress': case 'progress':
this.progress = info.event.percent; this.progress = info.event.percent;
this.doProgress(this.progress); // this.doProgress(this.progress);
break; break;
} }
} }
captureVideo() {
const video = this.videoNode.nativeElement;
const cvs = document.createElement('canvas');
const ctx = cvs.getContext("2d");
cvs.width = video.videoWidth;
cvs.height = video.videoHeight;
ctx.drawImage(video, 0, 0, cvs.width, cvs.height);
cvs.toBlob((blob)=>{
this.onCapture.emit(blob)
})
}
checkSelectFile(file) { checkSelectFile(file) {
if (!file.lastModified) { if (!file.lastModified) {
return false; return false;
} }
const delta = 500; const delta = 5000;
const isOverSize = (file.size / 1024 / 1024) < delta; const isOverSize = (file.size / 1024 / 1024) < delta;
if (!isOverSize) { if (!isOverSize) {
this.nzMessageService.error(`video must smaller than ${delta}MB!`); this.nzMessageService.error(`video must smaller than ${delta}MB!`);
...@@ -144,45 +273,16 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -144,45 +273,16 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
} }
uploadSuccess = (file) => { uploadSuccess = (file) => {
this.nzMessageService.info('Upload Success'); this.nzMessageService.info('Upload Success');
// this.updateStatus(false); // this.updateStatus(false);
this.uploading = false; this.uploading = false;
this.videoFile = null; this.videoFile = null;
// this.updateSource(url); // this.updateSource(url);
this.videoUrl = file.response.url; this.videoUrl = file.response.url;
// console.log(this.picUrl)
// this.uploadFinished(url);
// if (!inOSS) {
const vid = document.createElement('video');
vid.addEventListener('loadedmetadata', () => {
const height = vid.videoHeight;
const width = vid.videoWidth;
let duration = vid.duration;
if (duration) {
duration = duration * 1000;
}
file.height = height;
file.width = width;
file.duration = duration;
vid.preload = 'none';
vid.src = '';
vid.remove();
this.videoUploaded.emit(file.response); this.videoUploaded.emit(file.response);
setTimeout(() => {
// this.resService.updateVideo(id, {width, height, duration}).then( () => { this.safeVideoUrl();
// this.videoUploaded.emit({res_id: id, id, name, hash, url}); }, 2000);
// });
});
vid.src = file.response.url;
// } else {
// this.videoUploaded.emit(file.response);
// }
} }
......
import { ApplicationRef, ChangeDetectorRef, ElementRef, ViewChild } from "@angular/core";
export class ComponentBase {
// 储存数据用
saveKey = "";
// 储存对象
item: any = {};
ngOnChanges() { }
ngOnDestroy() { }
constructor(public appRef: ApplicationRef, public changeDetectorRef: ChangeDetectorRef) { }
ngOnInit() {
// 获取存储的数据
(<any>window).courseware.getData((data) => {
if (data) {
this.item = data;
this.itemStr = JSON.stringify(this.item, null, 4).trim();
}
this.init();
this.changeDetectorRef.markForCheck();
this.changeDetectorRef.detectChanges();
this.refresh();
}, this.saveKey);
}
@ViewChild("itemTextarea", { static: true }) itemTextarea: ElementRef;
copyData() {
this.itemTextarea.nativeElement.select();
document.execCommand("copy");
}
/**
* 储存图片数据
* @param e
*/
// onAssetUploadSuccess(e: any, ...key: Array<string>) {
// let item = this.item;
// for (let i = 0; i < key.length; i++) {
// if (i + 1 == key.length) {
// if (key[i] == "audio") {
// let sp = e.url.split(".mp3");
// let u = sp[0] + "_l.mp3";
// item[key[i]] = u;
// item["audioName"] = e.name || "";
// } else {
// item[key[i]] = e.url;
// }
// this.save();
// return;
// }
// item = item[key[i]];
// }
// }
/**
* 储存图片数据
* @param e
*/
onImageUploadSuccess(e, key, it = this.item) {
it[key] = e.url;
this.save();
}
/**
* 储存音频数据
* @param e
*/
onAudioUploadSuccess(e, key, it = this.item) {
let url = e.url;
let sp = url.split(".mp3");
let u = sp[0] + "_l.mp3";
it[key] = u;
it["audioName"] = e.name || "";
this.save();
}
save() {
(<any>window).courseware.setData(this.item, null, this.saveKey);
this.itemStr = JSON.stringify(this.item, null, 4).trim();
this.refresh();
console.log('this.item = ' + JSON.stringify(this.item));
}
itemStr = "";
load() {
this.itemStr = this.itemTextarea.nativeElement.value;
if (this.isJSON(this.itemStr)) {
this.item = JSON.parse(this.itemStr);
this.init();
this.changeDetectorRef.markForCheck();
this.changeDetectorRef.detectChanges();
this.refresh();
}
}
isJSON(str) {
if (typeof str == 'string') {
try {
var obj = JSON.parse(str);
if (typeof obj == 'object' && obj) {
return true;
}
return false;
} catch (e) {
return false;
}
}
return false;
}
/**
* 刷新 渲染页面
*/
refresh() {
setTimeout(() => {
this.appRef.tick();
}, 1);
}
init() { }
}
...@@ -4,5 +4,74 @@ ...@@ -4,5 +4,74 @@
.model-content { .model-content {
width: 100%; width: 100%;
height: 100%; height: 100%;
}
.radioPaire {
float: left;
margin: 3px;
border-style: dashed;
border-color: #000;
border-width: 1px;
}
.border {
border-radius: 20px;
border-style: dashed;
padding: 20px;
margin: 20px;
/*width: 500px; */
/*//border-radius: 20px;*/
/*//border-width: 2px;*/
/*//border-color: #000000;*/
}
.border-lite {
border: 2px dashed #ddd;
border-radius: 0.5rem;
padding: 10px;
margin: 10px;
}
.button-right {
border-style: dotted;
border-color: rgb(42, 142, 72);
width: 100px;
height: 50px;
color: rgb(42, 142, 72);
background-color: white;
}
.button-wrong {
border-style: dotted;
border-color: rgb(180, 0, 0);
width: 100px;
height: 50px;
color: rgb(180, 0, 0);
background-color: white;
}
.button-disable {
border-style: dotted;
border-color: black;
width: 100px;
height: 50px;
color: black;
background-color: white;
}
#drop-area {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
font-family: Arial, sans-serif;
font-size: 16px;
cursor: pointer;
}
#drop-area:hover {
background-color: #f0f0f0;
}
#drop-area:active {
background-color: #e0e0e0;
} }
...@@ -10,39 +10,35 @@ ...@@ -10,39 +10,35 @@
<textarea nz-input [(ngModel)]="item.video_title" (blur)="save()" style=" width: 350px; min-height: 60px;"></textarea> <textarea nz-input [(ngModel)]="item.video_title" (blur)="save()" style=" width: 350px; min-height: 60px;"></textarea>
</div> </div>
<div style="margin: 30px; display: flex; align-items: center; "> <div class="model-content">
<h3>上传视频封面:</h3>
<div style="margin-top: 10px; display: flex; ">
<div style="width: 300px;">
<app-upload-image-with-preview [picUrl]="item.video_cover_pic"
(imageUploaded)="onItemImgUploadSuccess($event, item, 'video_cover_pic')">
</app-upload-image-with-preview>
</div>
<button *ngIf="item.video_cover_pic" style="float: right;" nz-button nzType="" (click)="onDeletaImg(item, 'video_cover_pic')">
x
</button>
</div>
</div>
<div style="margin: 30px; display: flex; align-items: center; "> <div
<h3 style="">上传视频:</h3> id="drop-area"
<div style="display: flex; margin: 30px; width: 1000px"> (dragover)="onDragOver($event)"
(drop)="onDrop($event)"
<div>
<div style="width: 300px; border: 1px #ccc solid; border-radius: 15px; padding: 15px; background-color: rgb(255, 250, 243);">
<app-upload-video
[videoUrl]="item.video_url"
(videoUploaded)="onVideoUploaded($event)"
> >
</app-upload-video> 拖拽文件到此区域进行批量上传
</div> </div>
<div style="width: 100%; display: inline-flex;flex-direction: row;flex-wrap: wrap;">
<div style="width: 30% ; margin-left: 2.5%; min-width: 300px;margin-top: 20px;border: 2px dashed;padding: 20px 10px 5px 10px;"
align='center' *ngFor="let data of item.dataArr;index as i">
<div style="width: 100px; background-color: white; transform: translateY(-30px);" > 视频 {{i+1}}</div>
<div style="width: 80%;" align='center'>
<input type="text" nz-input [(ngModel)]="data.title" (blur)="save()">
</div> </div>
<br>
<app-upload-image-with-preview style="width: 80%;" [picUrl]="data.cover" (imageUploaded)="onImageUploadSuccess_new($event,i)">
</app-upload-image-with-preview>
<app-upload-video [videoUrl]="data.video_url.replace('.mp4', '_h.mp4')" [showUploadBtn]="true" (videoUploaded)="onVideoUploaded_new($event,i)" (onCapture)="handleOnCapture($event,i)" (parentRef)="onParentRef($event,i)"></app-upload-video>
<div style="padding: 5px 0;" align='center'>
<input type="text" nz-input [(ngModel)]="data.video_url" (blur)="save()">
</div>
<button style="margin-top: 10px; " (click)="onRemoveData(i)">删除</button>
</div>
<button style="width: 30% ;min-width: 300px;height: 170px; margin-top: 20px; margin-left:2.5%;"
(click)="onAddData()">添加视频</button>
</div> </div>
</div> </div>
...@@ -83,6 +79,11 @@ ...@@ -83,6 +79,11 @@
(总计: {{ques.time | time}}) (总计: {{ques.time | time}})
</span> </span>
</div> </div>
<div style="display: flex; align-items: center; gap: 5px;">
<button style="float: right; margin-right: -140px;" nz-button (click)="setFragment(ques,i)">
去设置
</button>
</div>
</div> </div>
<button style="float: right; margin-right: -140px;" nz-button nzType="danger" (click)="deletePageBtnClick(i)"> <button style="float: right; margin-right: -140px;" nz-button nzType="danger" (click)="deletePageBtnClick(i)">
删除节点 删除节点
...@@ -306,6 +307,84 @@ ...@@ -306,6 +307,84 @@
</div> </div>
<!-- 视频片段选择弹窗 -->
<nz-modal
[(nzVisible)]="isFragmentModalVisible"
nzTitle="选择视频时间节点"
(nzOnCancel)="fragmentModalCancel()"
[nzFooter]="null"
[nzWidth]="900">
<!-- 视频播放器 -->
<div style="padding: 20px;">
<div style="margin-bottom: 20px; text-align: center;">
<video
id="fragment-video"
[src]="fragmentVideoUrl"
controls
style="width: 100%; max-width: 800px;"
(loadedmetadata)="onFragmentVideoLoadedMetadata($event)"
(timeupdate)="onFragmentVideoTimeUpdate($event)">
您的浏览器不支持视频播放
</video>
</div>
<!-- 视频帧轨道 -->
<div style="margin-bottom: 20px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<span>当前时间: {{formatTime(fragmentSelectedTime)}}</span>
<span>总时长: {{formatTime(fragmentVideoDuration)}}</span>
</div>
<!-- 视频帧缩略图轨道 -->
<div *ngIf="videoThumbnails.length > 0" style="margin-bottom: 15px;">
<div style="display: flex; overflow-x: auto; padding: 10px 0; border: 1px solid #e8e8e8; border-radius: 4px; background-color: #fafafa;">
<div
*ngFor="let thumbnail of videoThumbnails; let i = index"
(click)="onThumbnailClick(getThumbnailTime(i))"
[style.border]="isThumbnailActive(i) ? '2px solid #1890ff' : '2px solid transparent'"
style="flex-shrink: 0; margin: 0 2px; cursor: pointer; border-radius: 2px; transition: border 0.2s;">
<img
[src]="thumbnail"
[alt]="'帧 ' + (i + 1)"
style="width: 80px; height: 45px; object-fit: cover; display: block; border-radius: 2px;">
<div style="font-size: 10px; text-align: center; color: #666; margin-top: 2px;">
{{formatTime(getThumbnailTime(i))}}
</div>
</div>
</div>
</div>
<!-- 生成缩略图提示 -->
<div *ngIf="isGeneratingThumbnails" style="text-align: center; padding: 10px; color: #999;">
<i nz-icon nzType="loading" nzTheme="outline"></i>
正在生成视频帧缩略图...
</div>
<!-- 时间滑块 -->
<nz-slider
[nzMin]="0"
[nzMax]="fragmentVideoDuration"
[nzStep]="0.1"
[ngModel]="fragmentSelectedTime"
(ngModelChange)="onFragmentTrackChange($event)"
(nzOnBeforeChange)="onFragmentTrackStart()"
(nzOnAfterChange)="onFragmentTrackEnd()"
[nzTooltipVisible]="'always'">
</nz-slider>
</div>
<!-- 确认和取消按钮 -->
<div style="text-align: right; margin-top: 30px;">
<button nz-button nzType="default" (click)="fragmentModalCancel()" style="margin-right: 10px;">
取消
</button>
<button nz-button nzType="primary" (click)="fragmentModalOk()">
确认节点
</button>
</div>
</div>
</nz-modal>
......
import { Component, EventEmitter, Input, OnDestroy, OnChanges, OnInit, Output, ApplicationRef, ChangeDetectorRef } from '@angular/core'; import { Component, EventEmitter, Input, OnDestroy, OnChanges, OnInit, Output, ApplicationRef, ChangeDetectorRef } from '@angular/core';
import { JsonPipe } from '@angular/common'; import { JsonPipe } from '@angular/common';
import { NzMessageService } from 'ng-zorro-antd/message'; import { NzMessageService } from 'ng-zorro-antd/message';
import { UploadChangeParam, UploadXHRArgs } from 'ng-zorro-antd/upload';
import { ComponentBase } from './ComponentBase';
@Component({ @Component({
...@@ -8,7 +10,7 @@ import { NzMessageService } from 'ng-zorro-antd/message'; ...@@ -8,7 +10,7 @@ import { NzMessageService } from 'ng-zorro-antd/message';
templateUrl: './form.component.html', templateUrl: './form.component.html',
styleUrls: ['./form.component.css'] styleUrls: ['./form.component.css']
}) })
export class FormComponent implements OnInit, OnChanges, OnDestroy { export class FormComponent extends ComponentBase implements OnInit, OnChanges, OnDestroy {
// 储存数据用 // 储存数据用
saveKey = "JJ_Vedio_Practice"; saveKey = "JJ_Vedio_Practice";
...@@ -16,7 +18,9 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -16,7 +18,9 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
baseData; baseData;
// 储存对象 // 储存对象
item; item: any = {
dataArr: []
};
//临时储存对象 //临时储存对象
tempItem; tempItem;
...@@ -31,6 +35,20 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -31,6 +35,20 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
isPreviewTemplate = false; isPreviewTemplate = false;
// 视频片段选择弹窗相关
isFragmentModalVisible = false;
currentFragmentQues = null;
currentFragmentIndex = -1;
fragmentVideoUrl = '';
fragmentVideoCurrentTime = 0;
fragmentVideoDuration = 0;
fragmentSelectedTime = 0;
fragmentVideoElement: HTMLVideoElement = null;
isDraggingTrack = false;
videoThumbnails: string[] = []; // 视频帧缩略图数组
isGeneratingThumbnails = false; // 是否正在生成缩略图
thumbnailCount = 20; // 生成的缩略图数量
...@@ -488,15 +506,14 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -488,15 +506,14 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
] ]
constructor(private appRef: ApplicationRef, private changeDetectorRef: ChangeDetectorRef, constructor(public appRef: ApplicationRef, public changeDetectorRef: ChangeDetectorRef,
private message: NzMessageService) { private message: NzMessageService) {
super(appRef, changeDetectorRef);
} }
ngOnInit() { ngOnInit() {
this.item = {};
// 获取存储的数据 // 获取存储的数据
(<any>window).courseware.getData((data) => { (<any>window).courseware.getData((data) => {
...@@ -519,6 +536,199 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -519,6 +536,199 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
}, this.saveKey); }, this.saveKey);
} }
setFragment(ques, i) {
console.log('i: ', i);
this.currentFragmentQues = ques;
this.currentFragmentIndex = i;
this.fragmentVideoUrl = this.item.video_url || '';
this.fragmentVideoCurrentTime = ques.time || 0;
this.fragmentSelectedTime = ques.time || 0;
this.videoThumbnails = [];
this.isFragmentModalVisible = true;
// 等待弹窗渲染后获取视频元素并生成缩略图
setTimeout(() => {
this.fragmentVideoElement = document.getElementById('fragment-video') as HTMLVideoElement;
if (this.fragmentVideoElement) {
if (this.fragmentSelectedTime > 0) {
this.fragmentVideoElement.currentTime = this.fragmentSelectedTime;
}
// 生成视频帧缩略图
this.generateVideoThumbnails();
}
}, 100);
}
// 视频片段弹窗 - 取消
fragmentModalCancel() {
this.isFragmentModalVisible = false;
this.currentFragmentQues = null;
this.currentFragmentIndex = -1;
this.fragmentVideoCurrentTime = 0;
this.fragmentVideoDuration = 0;
this.fragmentSelectedTime = 0;
this.fragmentVideoElement = null;
this.isDraggingTrack = false;
this.videoThumbnails = [];
this.isGeneratingThumbnails = false;
}
// 视频片段弹窗 - 确认
fragmentModalOk() {
if (this.currentFragmentQues) {
this.currentFragmentQues.time = this.fragmentSelectedTime;
this.save();
}
this.fragmentModalCancel();
}
// 视频加载元数据
onFragmentVideoLoadedMetadata(event) {
const video = event.target;
this.fragmentVideoDuration = video.duration || 0;
this.fragmentVideoElement = video;
if (this.fragmentSelectedTime > 0) {
video.currentTime = this.fragmentSelectedTime;
}
}
// 视频时间更新
onFragmentVideoTimeUpdate(event) {
if (!this.isDraggingTrack) {
const video = event.target;
this.fragmentVideoCurrentTime = video.currentTime;
this.fragmentSelectedTime = video.currentTime;
}
}
// 轨道拖动开始
onFragmentTrackStart() {
this.isDraggingTrack = true;
}
// 轨道拖动中
onFragmentTrackChange(value: number) {
this.fragmentSelectedTime = value;
if (this.fragmentVideoElement) {
this.fragmentVideoElement.currentTime = value;
}
}
// 轨道拖动结束
onFragmentTrackEnd() {
this.isDraggingTrack = false;
if (this.fragmentVideoElement) {
this.fragmentVideoElement.currentTime = this.fragmentSelectedTime;
}
}
// 格式化时间显示
formatTime(seconds: number): string {
if (!seconds || isNaN(seconds)) {
return '00:00';
}
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// 生成视频帧缩略图
async generateVideoThumbnails() {
if (!this.fragmentVideoElement || !this.fragmentVideoDuration) {
return;
}
this.isGeneratingThumbnails = true;
this.videoThumbnails = [];
const video = this.fragmentVideoElement;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 160; // 缩略图宽度
canvas.height = 90; // 缩略图高度 (16:9 比例)
const interval = this.fragmentVideoDuration / this.thumbnailCount;
const originalTime = video.currentTime;
const originalPaused = video.paused;
// 暂停视频
if (!video.paused) {
video.pause();
}
// 逐个提取帧
for (let i = 0; i < this.thumbnailCount; i++) {
const time = i * interval;
await this.seekToTime(video, time);
// 等待视频帧加载
await new Promise(resolve => setTimeout(resolve, 100));
// 绘制当前帧到 canvas
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 转换为 base64 图片
const thumbnail = canvas.toDataURL('image/jpeg', 0.7);
this.videoThumbnails.push(thumbnail);
// 更新视图
this.changeDetectorRef.detectChanges();
}
// 恢复视频状态
if (!originalPaused) {
video.play();
} else {
await this.seekToTime(video, originalTime);
}
this.isGeneratingThumbnails = false;
this.changeDetectorRef.detectChanges();
}
// 跳转到指定时间
seekToTime(video: HTMLVideoElement, time: number): Promise<void> {
return new Promise((resolve) => {
const onSeeked = () => {
video.removeEventListener('seeked', onSeeked);
resolve();
};
video.addEventListener('seeked', onSeeked);
video.currentTime = time;
});
}
// 点击缩略图跳转到对应时间
onThumbnailClick(time: number) {
this.fragmentSelectedTime = time;
if (this.fragmentVideoElement) {
this.fragmentVideoElement.currentTime = time;
}
}
// 获取缩略图对应的时间
getThumbnailTime(index: number): number {
if (!this.fragmentVideoDuration) {
return 0;
}
if (index >= this.thumbnailCount) {
return this.fragmentVideoDuration;
}
return (index * this.fragmentVideoDuration) / this.thumbnailCount;
}
// 判断缩略图是否处于激活状态(当前时间在该帧范围内)
isThumbnailActive(index: number): boolean {
if (!this.fragmentVideoDuration) {
return false;
}
const currentTime = this.getThumbnailTime(index);
const nextTime = index < this.thumbnailCount - 1
? this.getThumbnailTime(index + 1)
: this.fragmentVideoDuration;
return this.fragmentSelectedTime >= currentTime && this.fragmentSelectedTime < nextTime;
}
initItem() { initItem() {
} }
...@@ -706,12 +916,10 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -706,12 +916,10 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
* 储存图片数据 * 储存图片数据
* @param e * @param e
*/ */
onImageUploadSuccess(e, key, item) { onImageUploadSuccess(e) {
if (!item) {
item = this.item; this.item.file_url = e.url;
}
item[key] = e.url;
this.save(); this.save();
} }
...@@ -943,4 +1151,134 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -943,4 +1151,134 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
}, 1); }, 1);
} }
// 截图
handleOnCapture(e, index) {
console.log(e)
const href = URL.createObjectURL(e)
const box = document.createElement('a')
box.download = this.item.dataArr[index].title ? this.item.dataArr[index].title + ".jpg" : "未命名" + ".jpg"
box.href = href
box.click()
}
/**
* 储存图片数据
* @param e
*/
onImageUploadSuccess_new(e, index) {
this.item.dataArr[index].cover = e.url
this.save();
}
onVideoUploaded_new(e, index) {
this.item.dataArr[index].video_url = e.url
this.save();
}
/**
* 删除卡片
*/
onRemoveData(index) {
let data = this.item.dataArr[index]
if (data) {
this.item.dataArr.splice(index, 1)
this.save()
}
}
/** 添加卡片 */
onAddData() {
this.item.dataArr.push({
title: "",
cover: "",
video_url: ""
})
this.save()
}
onParentRef(e, index) {
const item = this.item.dataArr[index];
if(item.video_url || !item.batch){
return;
}
const uploadXHRArgs:UploadXHRArgs={
file: item.file,
name:'file',
onProgress:(en,file)=>{
e.handleChange({
file: file,
fileList: [file],
type:"progress",
event:en
});
},
onSuccess(ret, file, xhr) {
e.handleChange({
file: {...file,response:ret},
fileList: [file],
type:"success",
});
},
}
e.customReq(uploadXHRArgs);
const upFile: UploadChangeParam = {
file: item.file,
fileList: [item.file],
type:"start"
};
e.handleChange(upFile);
}
onDragOver(event: DragEvent) {
event.preventDefault();
}
onDrop(event: DragEvent) {
event.preventDefault();
const items = event.dataTransfer?.items;
if (items) {
this.handleItems(items);
}
}
handleItems(items: DataTransferItemList) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === "file") {
const entry = item.webkitGetAsEntry();
this.traverseFileTree(entry,item);
}
}
}
traverseFileTree(item: any,entity?:DataTransferItem) {
if (item.isFile) {
const file = entity.getAsFile();
this.item.dataArr.push({
title: item.name.replace(`.mp4`,``),
cover: "",
video_url: "",
file: file,
batch:true
});
this.save()
// 处理文件,可以上传等操作
} else if (item.isDirectory) {
this.item.unitTitle = item.name;
const directoryReader = item.createReader();
directoryReader.readEntries((entries: any) => {
for (let i = 0; i < entries.length; i++) {
this.traverseFileTree(entries[i],entries[i]);
}
});
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment