diff --git a/README.md b/README.md
index 9133bbd..b302c02 100644
--- a/README.md
+++ b/README.md
@@ -69,22 +69,23 @@ For ease of deployment, the following `docker-compose.yml` file can be used to o
```yaml
version: "3"
services:
- cast-fe:
- image: daystram/cast:fe
+ cast-be:
+ image: daystram/cast:be
ports:
- - "80:80"
+ - "8080:8080"
+ - "1935:1935"
+ env_file:
+ - /path_to_env_file/.env
restart: unless-stopped
cast-is: # no attached GPU
image: daystram/cast:is
env_file:
- /path_to_env_file/.env
restart: unless-stopped
- cast-be:
- image: daystram/cast:be
+ cast-fe:
+ image: daystram/cast:fe
ports:
- - "8080:8080"
- env_file:
- - /path_to_env_file/.env
+ - "80:80"
restart: unless-stopped
mongodb:
image: mongo:4.4-bionic
@@ -140,5 +141,14 @@ services:
This image is built on top of [NVIDIA's CUDA images](https://hub.docker.com/r/nvidia/cuda/) to enable FFmpeg harware acceleration on supported hosts. MP4Box is built from source, as seen on the [Dockerfile](https://github.com/daystram/cast/blob/master/cast-is/ingest-base.Dockerfile).
+### MongoDB Indexes
+For features to work properly, some indexes needs to be created in the MongoDB instance. Use the following command in `mongo` CLI to create indexes for `video` collection:
+
+```
+use MONGODB_NAME;
+db.video.createIndex({title: "text", description: "text"}, {collation: {locale: "simple"}});
+db.video.createIndex({hash: "hashed"});
+```
+
## License
This project is licensed under the [MIT License](https://github.com/daystram/cast/blob/master/LICENSE).
diff --git a/cast-be/controller/middleware/auth.go b/cast-be/controller/middleware/auth.go
index b23b444..3ce9d30 100644
--- a/cast-be/controller/middleware/auth.go
+++ b/cast-be/controller/middleware/auth.go
@@ -20,7 +20,7 @@ func AuthenticateAccessToken(ctx *context.Context) {
var accessToken string
if accessToken = strings.TrimPrefix(ctx.Input.Header("Authorization"), "Bearer "); accessToken == "" {
if accessToken = ctx.Input.Query("access_token"); accessToken == "" {
- ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+ ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
return
}
}
@@ -29,7 +29,7 @@ func AuthenticateAccessToken(ctx *context.Context) {
if info, err = verifyAccessToken(accessToken); err != nil || !info.Active {
log.Printf("[AuthFilter] Invalid access_token. %+v\n", err)
errMessage, _ := json.Marshal(map[string]interface{}{"message": "invalid access_token"})
- ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+ ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
_, _ = ctx.ResponseWriter.Write(errMessage)
return
}
diff --git a/cast-be/controller/v1/video.go b/cast-be/controller/v1/video.go
index 55dbf42..dab421b 100644
--- a/cast-be/controller/v1/video.go
+++ b/cast-be/controller/v1/video.go
@@ -129,36 +129,25 @@ func (c *VideoControllerAuth) EditVideo(_ string) datatransfers.Response {
fmt.Printf("[VideoControllerAuth::EditVideo] failed parsing video details. %+v\n", err)
return datatransfers.Response{Error: "Failed parsing video detail", Code: http.StatusInternalServerError}
}
+ if matchVideo, err := c.Handler.VideoDetails(video.Hash); err != nil {
+ log.Printf("[VideoControllerAuth::EditVideo] failed retrieving video. %+v\n", err)
+ return datatransfers.Response{Error: "Failed editing video", Code: http.StatusInternalServerError}
+ } else if matchVideo.Title != video.Title {
+ if err = c.Handler.CheckUniqueVideoTitle(video.Title); err != nil {
+ log.Printf("[VideoControllerAuth::EditVideo] title already used. %+v\n", err)
+ return datatransfers.Response{Error: "Title already used", Code: http.StatusConflict}
+ }
+ }
err = c.Handler.UpdateVideo(datatransfers.VideoEdit{
Hash: video.Hash,
Title: video.Title,
Description: video.Description,
Tags: strings.Split(video.Tags, ","),
- }, c.userID)
+ }, c.Controller, c.userID)
if err != nil {
fmt.Printf("[VideoControllerAuth::EditVideo] failed editing video. %+v\n", err)
return datatransfers.Response{Error: "Failed editing video", Code: http.StatusInternalServerError}
}
- if _, _, err = c.GetFile("thumbnail"); err != nil {
- if err == http.ErrMissingFile {
- return datatransfers.Response{Code: http.StatusOK}
- } else {
- fmt.Printf("[VideoControllerAuth::EditVideo] failed retrieving profile image. %+v\n", err)
- return datatransfers.Response{Error: "Failed retrieving profile image", Code: http.StatusInternalServerError}
- }
- }
- // TODO: use S3
- //// New thumbnail uploaded
- //err = c.SaveToFile("thumbnail", fmt.Sprintf("%s/thumbnail/%s.ori", config.AppConfig.UploadsDirectory, video.Hash))
- //if err != nil {
- // fmt.Printf("[VideoControllerAuth::EditVideo] failed saving thumbnail. %+v\n", err)
- // return datatransfers.Response{Error: "Failed saving thumbnail", Code: http.StatusInternalServerError}
- //}
- //err = c.Handler.NormalizeThumbnail(video.Hash)
- //if err != nil {
- // fmt.Printf("[VideoControllerAuth::EditVideo] failed normalizing thumbnail. %+v\n", err)
- // return datatransfers.Response{Error: "Failed normalizing thumbnail", Code: http.StatusInternalServerError}
- //}
return datatransfers.Response{Code: http.StatusOK}
}
@@ -191,6 +180,11 @@ func (c *VideoControllerAuth) UploadVideo(_ string) datatransfers.Response {
fmt.Printf("[VideoControllerAuth::UploadVideo] failed parsing video details. %+v\n", err)
return datatransfers.Response{Error: "Failed parsing video detail", Code: http.StatusInternalServerError}
}
+ err = c.Handler.CheckUniqueVideoTitle(upload.Title)
+ if err != nil {
+ log.Printf("[VideoControllerAuth::UploadVideo] title already used. %+v\n", err)
+ return datatransfers.Response{Error: "Title already used", Code: http.StatusConflict}
+ }
var videoID primitive.ObjectID
videoID, err = c.Handler.CreateVOD(datatransfers.VideoUpload{
Title: upload.Title,
diff --git a/cast-be/datatransfers/chat.go b/cast-be/datatransfers/chat.go
index e84ac10..f6b56ae 100644
--- a/cast-be/datatransfers/chat.go
+++ b/cast-be/datatransfers/chat.go
@@ -12,6 +12,7 @@ type ChatOutgoing struct {
// WS notification message
type NotificationOutgoing struct {
Message string `json:"message"`
+ Name string `json:"name"`
Username string `json:"username"`
Hash string `json:"hash"`
CreatedAt time.Time `json:"created_at"`
diff --git a/cast-be/datatransfers/user.go b/cast-be/datatransfers/user.go
index 879fc73..616348a 100644
--- a/cast-be/datatransfers/user.go
+++ b/cast-be/datatransfers/user.go
@@ -7,12 +7,14 @@ import (
type User struct {
ID string `json:"id" bson:"_id"`
Username string `json:"username" bson:"username"`
+ Name string `json:"name" bson:"name"`
CreatedAt time.Time `json:"created_at,omitempty" bson:"created_at"`
}
type UserDetail struct {
ID string `json:"id"`
Username string `json:"username"`
+ Name string `json:"name"`
Subscribers int `json:"subscribers" bson:"subscribers"`
Views int `json:"views" bson:"views"`
Uploads int `json:"uploads" bson:"uploads"`
@@ -21,6 +23,7 @@ type UserDetail struct {
type UserItem struct {
ID string `json:"-" bson:"_id"`
Username string `json:"username" bson:"username"`
+ Name string `json:"name" bson:"name"`
Subscribers int `json:"subscribers" bson:"subscribers"`
}
diff --git a/cast-be/handlers/auth.go b/cast-be/handlers/auth.go
index 426436f..523d242 100644
--- a/cast-be/handlers/auth.go
+++ b/cast-be/handlers/auth.go
@@ -61,6 +61,7 @@ func parseIDToken(idToken string) (user datatransfers.User, err error) {
user = datatransfers.User{
ID: claims["sub"].(string),
Username: claims["preferred_username"].(string),
+ Name: fmt.Sprintf("%s %s", claims["given_name"], claims["family_name"]),
CreatedAt: time.Now(),
}
return
diff --git a/cast-be/handlers/init.go b/cast-be/handlers/init.go
index d70f7e3..0f3f29e 100644
--- a/cast-be/handlers/init.go
+++ b/cast-be/handlers/init.go
@@ -76,7 +76,7 @@ type Handler interface {
VideoDetails(hash string) (video data.Video, err error)
CreateVOD(upload data.VideoUpload, controller beego.Controller, userID string) (ID primitive.ObjectID, err error)
DeleteVideo(ID primitive.ObjectID, userID string) (err error)
- UpdateVideo(video data.VideoEdit, userID string) (err error)
+ UpdateVideo(video data.VideoEdit, controller beego.Controller, userID string) (err error)
CheckUniqueVideoTitle(title string) (err error)
LikeVideo(userID string, hash string, like bool) (err error)
Subscribe(userID string, username string, subscribe bool) (err error)
diff --git a/cast-be/handlers/rtmp.go b/cast-be/handlers/rtmp.go
index a989b23..d088988 100644
--- a/cast-be/handlers/rtmp.go
+++ b/cast-be/handlers/rtmp.go
@@ -4,11 +4,12 @@ import (
"errors"
"fmt"
"io"
+ "log"
"net/http"
"path"
"sync"
"time"
-
+
"github.com/nareix/joy4/av/avutil"
"github.com/nareix/joy4/av/pubsub"
"github.com/nareix/joy4/format/flv"
@@ -64,7 +65,8 @@ func (m *module) CreateRTMPUpLink() {
}
fmt.Printf("[RTMPUpLink] UpLink for %s connected\n", username)
m.BroadcastNotificationSubscriber(video.Author.ID, datatransfers.NotificationOutgoing{
- Message: fmt.Sprintf("%s just went live! Watch now!", video.Author.Username),
+ Message: fmt.Sprintf("%s just went live! Watch now!", video.Author.Name),
+ Name: video.Author.Name,
Username: video.Author.Username,
Hash: video.Hash,
CreatedAt: time.Now(),
@@ -86,7 +88,7 @@ func (m *module) CreateRTMPUpLink() {
}
m.live.uplink.Addr = fmt.Sprintf(":%d", config.AppConfig.RTMPPort)
go m.live.uplink.ListenAndServe()
- fmt.Printf("[CreateRTMPUpLink] RTMP UpLink Window opened at port %d\n", config.AppConfig.RTMPPort)
+ log.Printf("[CreateRTMPUpLink] RTMP UpLink Window opened at port %d\n", config.AppConfig.RTMPPort)
}
func (m *module) ControlUpLinkWindow(userID string, open bool) (err error) {
diff --git a/cast-be/handlers/transcode.go b/cast-be/handlers/transcode.go
index cc6aad8..66d7e3b 100644
--- a/cast-be/handlers/transcode.go
+++ b/cast-be/handlers/transcode.go
@@ -48,6 +48,7 @@ func (m *module) TranscodeListenerWorker() {
if resolution >= 1 {
m.PushNotification(video.Author.ID, datatransfers.NotificationOutgoing{
Message: fmt.Sprintf("%s is now ready in %s!", video.Title, constants.VideoResolutions[resolution]),
+ Name: video.Author.Name,
Username: video.Author.Username,
Hash: video.Hash,
CreatedAt: time.Now(),
@@ -55,7 +56,8 @@ func (m *module) TranscodeListenerWorker() {
}
if resolution == 1 {
m.BroadcastNotificationSubscriber(video.Author.ID, datatransfers.NotificationOutgoing{
- Message: fmt.Sprintf("%s just uploaded %s! Watch now!", video.Author.Username, video.Title),
+ Message: fmt.Sprintf("%s just uploaded %s! Watch now!", video.Author.Name, video.Title),
+ Name: video.Author.Name,
Username: video.Author.Username,
Hash: video.Hash,
CreatedAt: time.Now(),
diff --git a/cast-be/handlers/user.go b/cast-be/handlers/user.go
index 6612e4c..c8ebae5 100644
--- a/cast-be/handlers/user.go
+++ b/cast-be/handlers/user.go
@@ -27,6 +27,7 @@ func (m *module) UserDetails(userID string) (detail data.UserDetail, err error)
detail = data.UserDetail{
ID: user.ID,
Username: user.Username,
+ Name: user.Name,
Subscribers: subscriberCount,
Views: views,
Uploads: len(videos),
diff --git a/cast-be/handlers/video.go b/cast-be/handlers/video.go
index 8c5d376..3ebdae2 100644
--- a/cast-be/handlers/video.go
+++ b/cast-be/handlers/video.go
@@ -4,6 +4,8 @@ import (
"bytes"
"errors"
"fmt"
+ "mime/multipart"
+ "net/http"
"time"
"github.com/astaxie/beego"
@@ -66,13 +68,18 @@ func (m *module) SearchVideo(query string, _ []string, count, offset int) (video
}
func (m *module) VideoDetails(hash string) (video data.Video, err error) {
+ var author data.UserDetail
var comments []data.Comment
if video, err = m.db.videoOrm.GetOneByHash(hash); err != nil {
return data.Video{}, errors.New(fmt.Sprintf("[VideoDetails] video with hash %s not found. %+v", hash, err))
}
+ if author, err = m.UserDetails(video.Author.ID); err != nil {
+ return data.Video{}, errors.New(fmt.Sprintf("[VideoDetails] failed video author. %+v", err))
+ }
if comments, err = m.db.commentOrm.GetAllByHash(hash); err != nil {
return data.Video{}, errors.New(fmt.Sprintf("[VideoDetails] failed getting comment list for %s. %+v", hash, err))
}
+ video.Author.Subscribers = author.Subscribers
video.Views++
video.Comments = comments
if video.Type == constants.VideoTypeVOD {
@@ -165,7 +172,7 @@ func (m *module) DeleteVideo(ID primitive.ObjectID, userID string) (err error) {
return m.db.videoOrm.DeleteOneByID(ID)
}
-func (m *module) UpdateVideo(video data.VideoEdit, userID string) (err error) {
+func (m *module) UpdateVideo(video data.VideoEdit, controller beego.Controller, userID string) (err error) {
if err = m.db.videoOrm.EditVideo(data.VideoInsert{
Hash: video.Hash,
Title: video.Title,
@@ -175,6 +182,26 @@ func (m *module) UpdateVideo(video data.VideoEdit, userID string) (err error) {
}); err != nil {
return errors.New(fmt.Sprintf("[UpdateVideo] error updating video. %+v", err))
}
+ // Retrieve thumbnail
+ var thumbnail multipart.File
+ if thumbnail, _, err = controller.GetFile("thumbnail"); err!= nil {
+ if err == http.ErrMissingFile {
+ return nil
+ } else {
+ return fmt.Errorf("[UpdateVideo] Failed retrieving thumbnail image. %+v\n", err)
+ }
+ }
+ var result bytes.Buffer
+ if result, err = util.NormalizeImage(thumbnail, constants.ThumbnailWidth, constants.ThumbnailHeight); err != nil {
+ return fmt.Errorf("[UpdateVideo] Failed normalizing thumbnail image. %+v", err)
+ }
+ if _, err = m.s3.PutObject(&s3.PutObjectInput{
+ Bucket: aws.String(config.AppConfig.S3Bucket),
+ Key: aws.String(fmt.Sprintf("%s/%s.jpg", constants.ThumbnailRootDir, video.Hash)),
+ Body: bytes.NewReader(result.Bytes()),
+ }); err != nil {
+ return fmt.Errorf("[UpdateVideo] Failed saving thumbnail image. %+v", err)
+ }
return
}
@@ -225,7 +252,8 @@ func (m *module) Subscribe(userID string, username string, subscribe bool) (err
CreatedAt: time.Now(),
})
m.PushNotification(author.ID, data.NotificationOutgoing{
- Message: fmt.Sprintf("%s just subscribed!", user.Username),
+ Message: fmt.Sprintf("%s just subscribed!", user.Name),
+ Name: user.Name,
Username: user.Username,
CreatedAt: time.Now(),
})
@@ -270,6 +298,7 @@ func (m *module) CommentVideo(userID string, hash, content string) (comment data
Content: content,
Author: data.UserItem{
Username: user.Username,
+ Name: user.Name,
},
CreatedAt: now,
}, nil
diff --git a/cast-be/routers/router.go b/cast-be/routers/router.go
index a914996..3deeaa7 100644
--- a/cast-be/routers/router.go
+++ b/cast-be/routers/router.go
@@ -2,7 +2,6 @@ package routers
import (
"context"
- "fmt"
"log"
"github.com/astaxie/beego"
@@ -80,7 +79,7 @@ func init() {
// Init Transcoder Listener
go h.TranscodeListenerWorker()
- fmt.Printf("[Initialization] Initialization complete!\n")
+ log.Printf("[Initialization] Initialization complete!\n")
apiV1 := beego.NewNamespace("api/v1",
beego.NSNamespace("/ping",
diff --git a/cast-fe/package.json b/cast-fe/package.json
index cceae6b..7e4bc98 100644
--- a/cast-fe/package.json
+++ b/cast-fe/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@daystram/ratify-client": "^1.3.0",
+ "@daystram/ratify-client": "^1.4.0",
"axios": ">=0.21.1",
"bootstrap": "^4.4.1",
"bs-custom-file-input": "^1.3.4",
diff --git a/cast-fe/public/index.html b/cast-fe/public/index.html
index 889e6cb..927224f 100644
--- a/cast-fe/public/index.html
+++ b/cast-fe/public/index.html
@@ -11,7 +11,7 @@
/>
-
+
cast
diff --git a/cast-fe/src/Routes.jsx b/cast-fe/src/Routes.jsx
index 5f0f80d..18ec4f6 100644
--- a/cast-fe/src/Routes.jsx
+++ b/cast-fe/src/Routes.jsx
@@ -7,14 +7,14 @@ import {
Home,
Liked,
Live,
+ Manage,
Profile,
Scene,
Search,
Subscribed,
Trending,
-} from "./components";
-import Manage from "./components/Manage";
-import auth from "./helper/auth";
+} from "./views";
+import { login, logout, callback, authManager } from "./helper/auth";
const Routes = () => {
return (
@@ -28,16 +28,12 @@ const Routes = () => {
- {/* */}
-
-
-
- {/* */}
- {/* */}
- {/* */}
+
+
+
@@ -49,7 +45,7 @@ const PrivateRoute = ({ component: Component, ...rest }) => (
- auth().is_authenticated() ? (
+ authManager.isAuthenticated() ? (
) : (
(
- auth().is_authenticated() ? (
+ authManager.isAuthenticated() ? (
) : (
diff --git a/cast-fe/src/apis/api.js b/cast-fe/src/apis/api.js
new file mode 100644
index 0000000..6754caf
--- /dev/null
+++ b/cast-fe/src/apis/api.js
@@ -0,0 +1,140 @@
+import axios from "axios";
+import { ACCESS_TOKEN } from "@daystram/ratify-client";
+import { authManager, refreshAuth } from "../helper/auth";
+
+const baseAPI = `${
+ process.env.NODE_ENV === "development"
+ ? process.env.REACT_APP_DEV_BASE_API
+ : ""
+}/api/v1`;
+const baseCDN = "https://cdn.daystram.com/cast";
+const baseWS =
+ process.env.NODE_ENV === "development"
+ ? `${process.env.REACT_APP_DEV_BASE_WS}/api/v1`
+ : `wss://${window.location.hostname}/api/v1`;
+
+const apiClient = axios.create({
+ baseURL: baseAPI,
+});
+
+apiClient.interceptors.response.use(
+ (response) => {
+ return response;
+ },
+ (error) => {
+ if (error.response.status === 401) {
+ refreshAuth(window.location.href);
+ }
+ return Promise.reject(error);
+ }
+);
+
+const withAuth = () => ({
+ headers: {
+ Authorization: `Bearer ${authManager.getToken(ACCESS_TOKEN)}`,
+ },
+});
+
+export default {
+ cdn: {
+ thumbnail(hash) {
+ return `${baseCDN}/thumbnail/${hash}.jpg`;
+ },
+ vod(hash) {
+ return `${baseCDN}/video/${hash}/manifest.mpd`;
+ },
+ download(hash) {
+ return `${baseCDN}/video/${hash}/video.mp4`;
+ },
+ },
+ live: {
+ stream(username) {
+ return `${baseAPI}/live/stream/${username}`;
+ },
+ window: {
+ status() {
+ return apiClient.get(`/p/live/window`, withAuth());
+ },
+ set(open) {
+ return apiClient.put(`/p/live/window?open=${open}`, {}, withAuth());
+ },
+ },
+ },
+ ws: {
+ chat(hash) {
+ const token = authManager.getToken(ACCESS_TOKEN);
+ return `${baseWS}${token && "/p"}/ws/chat/${hash}${
+ token && "?access_token=" + token
+ }`;
+ },
+ notification() {
+ return `${baseWS}/p/ws/notification?access_token=${authManager.getToken(
+ ACCESS_TOKEN
+ )}`;
+ },
+ },
+ cast: {
+ list(params) {
+ return apiClient.get(`/video/list`, { params });
+ },
+ listCurated(params) {
+ return apiClient.get(`/p/video/list`, { params, ...withAuth() });
+ },
+ search(params) {
+ return apiClient.get(`/video/search`, { params });
+ },
+ detail(params) {
+ return apiClient.get(`/video/details`, { params });
+ },
+ upload(form, onUploadProgress) {
+ return apiClient.post(`/p/video/upload`, form, {
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Content-Type": "multipart/form-data",
+ ...withAuth().headers,
+ },
+ onUploadProgress,
+ });
+ },
+ edit(form) {
+ return apiClient.put(`/p/video/edit`, form, {
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Content-Type": "multipart/form-data",
+ ...withAuth().headers,
+ },
+ });
+ },
+ remove(hash) {
+ return apiClient.delete(`/p/video/delete`, {
+ params: { hash },
+ ...withAuth(),
+ });
+ },
+ like(data) {
+ return apiClient.post(`/p/video/like`, data, withAuth());
+ },
+ comment(data) {
+ return apiClient.post(`/p/video/comment`, data, withAuth());
+ },
+ titleCheck(title) {
+ return apiClient.get(`/p/video/check`, {
+ params: { title },
+ ...withAuth(),
+ });
+ },
+ },
+ user: {
+ detail() {
+ return apiClient.get(`/p/user/info`, withAuth());
+ },
+ subscribe(data) {
+ return apiClient.post(`/p/video/subscribe`, data, withAuth());
+ },
+ },
+ auth: {
+ register(idToken) {
+ return apiClient.post(`/p/auth/check`, { id_token: idToken }, withAuth());
+ },
+ },
+};
diff --git a/cast-fe/src/components/Author.jsx b/cast-fe/src/components/Author.jsx
index 7a4f427..de22f67 100644
--- a/cast-fe/src/components/Author.jsx
+++ b/cast-fe/src/components/Author.jsx
@@ -1,7 +1,6 @@
import React from "react";
import { Button, Col, Image } from "react-bootstrap";
import abbreviate from "../helper/abbreviate";
-import urls from "../helper/url";
function Author(props) {
function handleSubscribe(e) {
@@ -12,7 +11,7 @@ function Author(props) {
<>
@@ -37,13 +39,11 @@ function Cast(props) {
-
{props.video.title}
diff --git a/cast-fe/src/components/CastEditable.jsx b/cast-fe/src/components/CastEditable.jsx
index 2017be9..227d990 100644
--- a/cast-fe/src/components/CastEditable.jsx
+++ b/cast-fe/src/components/CastEditable.jsx
@@ -12,12 +12,11 @@ import {
Spinner,
} from "react-bootstrap";
import Dropzone from "react-dropzone";
-import urls from "../helper/url";
+import { currentHash } from "../helper/url";
import format from "../helper/format";
-import axios from "axios";
import { Prompt, withRouter } from "react-router-dom";
import { WithContext as ReactTags } from "react-tag-input";
-import "./tags.css";
+import "../styles/tags.css";
import { THUMBNAIL_MAX_SIZE } from "../constants/file";
import {
VIDEO_DESC_CHAR_LIMIT,
@@ -25,6 +24,7 @@ import {
VIDEO_TAG_COUNT,
VIDEO_TITLE_CHAR_LIMIT,
} from "../constants/video";
+import api from "../apis/api";
const resolutions = ["Processing", "240p", "360p", "480p", "720p", "1080p"];
let timeout = {};
@@ -40,7 +40,7 @@ class CastEditable extends Component {
})
: [],
description: this.props.video.description,
- thumbnail: urls().thumbnail(this.props.video.hash),
+ thumbnail: api.cdn.thumbnail(this.props.video.hash),
error_title: "",
error_tags: "",
error_description: "",
@@ -218,12 +218,8 @@ class CastEditable extends Component {
checkAvailability(value) {
clearTimeout(timeout);
timeout = setTimeout(() => {
- axios
- .get(urls().title_check(), {
- params: {
- title: value.trim(),
- },
- })
+ api.cast
+ .titleCheck(value.trim())
.then((response) => {
if (response.data.code !== 200 && this.state.editing) {
this.setState({ error_title: response.data.error });
@@ -259,13 +255,8 @@ class CastEditable extends Component {
form.append("tags", this.state.tags.map((tag) => tag.text).join(","));
if (this.state.new_thumbnail)
form.append("thumbnail", this.state.new_thumbnail);
- axios
- .put(urls().edit_video(), form, {
- headers: {
- "Access-Control-Allow-Origin": "*",
- "Content-Type": "multipart/form-data",
- },
- })
+ api.cast
+ .edit(form)
.then((response) => {
clearTimeout(timeout);
if (response.data.code === 200) {
@@ -302,12 +293,8 @@ class CastEditable extends Component {
deleteVideo() {
this.setState({ error_delete: "", loading_delete: true });
- axios
- .delete(urls().delete(), {
- params: {
- hash: this.props.video.hash,
- },
- })
+ api.cast
+ .remove(this.props.video.hash)
.then((response) => {
if (response.data.code === 200) {
this.setState({ loading_delete: false, prompt: false });
@@ -331,16 +318,13 @@ class CastEditable extends Component {
openVideo() {
switch (this.props.video.type) {
case "live":
- if (
- this.props.video.is_live &&
- this.props.video.hash !== urls().current_hash()
- )
+ if (this.props.video.is_live && this.props.video.hash !== currentHash())
this.props.history.push(`/w/${this.props.video.hash}`);
break;
case "vod":
if (
this.props.video.resolutions &&
- this.props.video.hash !== urls().current_hash()
+ this.props.video.hash !== currentHash()
)
this.props.history.push(`/w/${this.props.video.hash}`);
break;
@@ -429,7 +413,7 @@ class CastEditable extends Component {
{this.state.error_edit}
)}
{this.state.editing ? (
-
{this.state.tags &&
Object.values(this.state.tags).map((tag) => (
-
+
{tag.text}
))}
@@ -523,7 +507,7 @@ class CastEditable extends Component {
)}
{this.state.editing ? (
-
{
- this.setState({ loading: false });
- if (response.data.code === 200) {
- this.setState({ valid: true });
- } else {
- this.setState({ error_reset: "Password reset link invalid!" });
- }
- })
- .catch((error) => {
- console.log(error);
- this.setState({ error_reset: "An error has occurred!" });
- this.setState({ loading: false });
- });
- }
- }
-
- handleChange(e) {
- this.setState({ error_reset: "", error_update: "" });
- this.setState({ [e.target.name]: e.target.value });
- this.validate(e.target.name, e.target.value);
- }
-
- validate(field, value) {
- switch (field) {
- case "email":
- if (!value.trim()) {
- this.setState({ error_email: "Please enter your email" });
- return false;
- }
- let emailRe = /.+@.+\..+/;
- if (!emailRe.test(value.trim())) {
- this.setState({ error_email: "Invalid email address" });
- return false;
- }
- this.setState({ error_email: "" });
- return true;
- case "password":
- if (!value) {
- this.setState({ error_password: "Please enter your password" });
- return false;
- }
- if (value.length < 8) {
- this.setState({
- error_password: "Password must be at least 8 characters",
- });
- return false;
- }
- this.setState({ error_password: "" });
- return true;
- case "password2":
- if (!value) {
- this.setState({ error_password2: "Please re-enter your password" });
- return false;
- }
- if (value !== this.state.password) {
- this.setState({ error_password2: "Passwords do not match" });
- return false;
- }
- this.setState({ error_password2: "" });
- return true;
- default:
- return false;
- }
- }
-
- sendLink(e) {
- e.preventDefault();
- let ok = true;
- ok &= !this.state.error_email;
- if (!ok) return;
- this.setState({ loading: true, success: false });
- axios
- .post(urls().reset_password(), {
- email: this.state.email.trim(),
- })
- .then((response) => {
- this.setState({ loading: false });
- switch (response.data.code) {
- case 200:
- this.setState({ success: true });
- break;
- case 403:
- this.setState({ unverified: false });
- break;
- case 404:
- this.setState({ error_reset: "Email not registered!" });
- break;
- default:
- this.setState({ error_reset: "An error has occurred!" });
- break;
- }
- })
- .catch((error) => {
- console.log(error);
- this.setState({
- error_reset: "An error has occurred!",
- loading: false,
- });
- });
- }
-
- updatePassword(e) {
- e.preventDefault();
- let ok = true;
- this.setState({ success: false });
- if (!this.state.attempted) {
- this.setState({ attempted: true });
- ok &= this.validate("password", this.state.password);
- ok &= this.validate("password2", this.state.password2);
- } else {
- ok &= !this.state.error_password;
- ok &= !this.state.error_password2;
- }
- if (!ok) return;
- this.setState({ loading: true });
- axios
- .put(urls().update_password(), {
- key: this.state.key,
- password: this.state.password.trim(),
- })
- .then((response) => {
- this.setState({ loading: false, password: "", password2: "" });
- if (response.data.code === 200) {
- this.setState({
- success: true,
- password: "",
- password2: "",
- error_password: "",
- error_password2: "",
- });
- } else {
- this.setState({ error_update: "An error has occurred!" });
- }
- })
- .catch((error) => {
- console.log(error);
- this.setState({
- error_update: "An error has occurred!",
- loading: false,
- });
- });
- }
-
- render() {
- let strength = zxcvbn(this.state.password).score;
- return (
-
- Forget Password
- {this.state.key ? (
- <>
- {this.state.error_reset && (
-
- Invalid Link!
-
- Your password reset link is invalid! Please request for a new
- password reset link.
-
-
-
-
-
-
- )}
- {this.state.valid && (
- <>
- {this.state.success ? (
-
- Welcome!
-
- Your password has been successfully reset. You can now log
- in with your new password!
-
-
-
-
-
-
- ) : (
- <>
- Enter your new password below.
- {this.state.error_update && (
-
- {this.state.error_update}
-
- )}
-
-
-
-
-
- {this.state.error_password}
-
-
-
-
-
- {this.state.error_password2}
-
-
-
-
-
-
-
- >
- )}
- >
- )}
- {!this.state.valid && this.state.loading && (
-
- )}
- >
- ) : (
- <>
- Enter your email below to get your password reset link.
- {this.state.error_reset && (
- {this.state.error_reset}
- )}
- {this.state.success && (
- Password reset link sent!
- )}
- {this.state.unverified && (
-
- Email Unverified!
-
- Your account email has not been verified. Please check your
- email for a verification link before resetting your password.
-
-
-
-
-
-
- )}
-
-
-
- {this.state.error_email}
-
-
-
- >
- )}
-
- );
- }
-}
-
-let style = {
- h1: {
- fontFamily: "Comfortaa",
- },
- content_container: {
- maxWidth: 480,
- },
- spinner: {
- margin: "32px auto 64px auto",
- display: "block",
- },
-};
-
-export default withRouter(Forget);
diff --git a/cast-fe/src/components/player/HybridPlayer.jsx b/cast-fe/src/components/HybridPlayer.jsx
similarity index 85%
rename from cast-fe/src/components/player/HybridPlayer.jsx
rename to cast-fe/src/components/HybridPlayer.jsx
index 290dd12..8bc2dfc 100644
--- a/cast-fe/src/components/player/HybridPlayer.jsx
+++ b/cast-fe/src/components/HybridPlayer.jsx
@@ -1,16 +1,18 @@
import React from "react";
import "dashjs";
import videojs from "video.js";
+// import "videojs-contrib-dash" // must disable for quality selector to appear;
+import "videojs-flvjs-es6";
import "videojs-contrib-quality-levels";
import "videojs-http-source-selector";
-import "videojs-contrib-dash";
+
import "video.js/dist/video-js.css";
-import "videojs-flvjs-es6";
-import "./player.css";
+import "../styles/player.css";
class HybridPlayer extends React.Component {
componentDidMount() {
this.initPlayer();
+ this.updatePlayer();
}
componentDidUpdate(prevProps, prevState, snapshot) {
@@ -23,7 +25,7 @@ class HybridPlayer extends React.Component {
componentWillUnmount() {
if (this.player) {
console.log("[HybridPlayer] Dismount");
- this.player.dispose();
+ // this.player.dispose(); // causes SourceBufferSink errors
}
}
@@ -33,10 +35,10 @@ class HybridPlayer extends React.Component {
fluid: true,
responsive: true,
aspectRatio: "16:9",
- // liveui: true,
- preload: "true",
+ preload: "false",
controls: true,
userActions: { hotkeys: true },
+ // liveui: true,
plugins: {
httpSourceSelector: {
default: "auto",
@@ -45,30 +47,28 @@ class HybridPlayer extends React.Component {
flvjs: {
mediaDataSource: {
isLive: true,
- cors: false, // TODO: NOTICE!
+ cors: true,
withCredentials: false,
},
},
- // autoplay: this.props.live,
- // poster: this.props.thumbnail,
};
this.player = videojs(this.videoNode, options);
+ this.player.qualityLevels();
this.player.httpSourceSelector();
}
updatePlayer() {
- console.log(this.props.url);
if (!this.props.url) return;
this.player.pause();
+ this.player.reset();
this.player.src({
src: this.props.url,
type: this.props.live ? "video/x-flv" : "application/dash+xml",
});
this.player.autoplay(this.props.live);
if (this.props.live) this.player.play();
- // else this.player.pause();
- this.player.load();
this.player.poster(this.props.thumbnail);
+ this.player.load();
}
render() {
diff --git a/cast-fe/src/components/List.jsx b/cast-fe/src/components/List.jsx
index e146fdd..9c23b52 100644
--- a/cast-fe/src/components/List.jsx
+++ b/cast-fe/src/components/List.jsx
@@ -2,13 +2,12 @@ import React, { Component } from "react";
import { Col, Row, Spinner } from "react-bootstrap";
import InfiniteScroll from "react-infinite-scroller";
import Cast from "./Cast";
-import axios from "axios";
-import urls from "../helper/url";
import {
VIDEO_LIST_LIKED,
VIDEO_LIST_PAGE_SIZE,
VIDEO_LIST_SUBSCRIBED,
} from "../constants/video";
+import api from "../apis/api";
class List extends Component {
constructor(props) {
@@ -28,38 +27,31 @@ class List extends Component {
}
fetchVideos() {
- let target;
- let config;
+ let request;
if (this.props.search) {
- target = urls().search();
- config = {
- params: {
- query: this.props.query.trim(),
- count: VIDEO_LIST_PAGE_SIZE,
- offset: VIDEO_LIST_PAGE_SIZE * this.state.page,
- },
- };
+ request = api.cast.search({
+ query: this.props.query.trim(),
+ count: VIDEO_LIST_PAGE_SIZE,
+ offset: VIDEO_LIST_PAGE_SIZE * this.state.page,
+ });
} else {
+ const params = {
+ variant: this.props.variant,
+ count: VIDEO_LIST_PAGE_SIZE,
+ offset: VIDEO_LIST_PAGE_SIZE * this.state.page,
+ };
switch (this.props.variant) {
case VIDEO_LIST_LIKED:
- target = urls().list_authed();
+ request = api.cast.listCurated(params);
break;
case VIDEO_LIST_SUBSCRIBED:
- target = urls().list_authed();
+ request = api.cast.listCurated(params);
break;
default:
- target = urls().list();
+ request = api.cast.list(params);
}
- config = {
- params: {
- variant: this.props.variant,
- count: VIDEO_LIST_PAGE_SIZE,
- offset: VIDEO_LIST_PAGE_SIZE * this.state.page,
- },
- };
}
- axios
- .get(target, config)
+ request
.then((response) => {
if (response.data.code === 200) {
if (!response.data.data) {
diff --git a/cast-fe/src/components/Navigation.jsx b/cast-fe/src/components/Navigation.jsx
index 6f61ef1..84fed5f 100644
--- a/cast-fe/src/components/Navigation.jsx
+++ b/cast-fe/src/components/Navigation.jsx
@@ -12,12 +12,11 @@ import {
} from "react-bootstrap";
import MediaQuery from "react-responsive";
import logo from "./logo.svg";
-import auth, { refreshAuth } from "../helper/auth";
+import { authManager } from "../helper/auth";
import { MOBILE_BP } from "../constants/breakpoint";
import Sidebar from "./Sidebar";
import SidebarProfile from "./SidebarProfile";
-import axios from "axios";
-
+import { ProfileImage } from "./index";
function Navigation() {
const [query, setQuery] = useState(
new URLSearchParams(useLocation().search).get("query") || ""
@@ -25,37 +24,16 @@ function Navigation() {
const [expanded, setExpanded] = useState(false);
const inputRef = useRef();
const history = useHistory();
- const user = auth().user();
- axios.interceptors.response.use(
- (response) => response,
- (error) => {
- if (error.response.status === 403) {
- refreshAuth();
- }
- }
- );
- let profileButton = auth().is_authenticated() ? (
- {
history.push("/profile");
}}
- >
- {user.given_name[0] + user.family_name[0]}
-
+ />
) : (
<>