Skip to content

Commit

Permalink
Did HLS
Browse files Browse the repository at this point in the history
  • Loading branch information
Puneet-NJ committed Jan 14, 2025
1 parent 66b9ec1 commit a34162a
Show file tree
Hide file tree
Showing 24 changed files with 6,658 additions and 5,146 deletions.
13 changes: 13 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@types/express": "^5.0.0",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.10.2",
"aws-cloudfront-sign": "^3.0.2",
"aws-sdk": "^2.1692.0",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.7",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
Warnings:
- You are about to drop the column `contentUrl` on the `CourseContent` table. All the data in the column will be lost.
- You are about to drop the column `isUploaded` on the `CourseContent` table. All the data in the column will be lost.
- Added the required column `status` to the `CourseContent` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "ContentStatus" AS ENUM ('PROCESSING', 'PROCESSED', 'PENDING', 'FAILED');

-- AlterTable
ALTER TABLE "CourseContent" DROP COLUMN "contentUrl",
DROP COLUMN "isUploaded",
ADD COLUMN "status" "ContentStatus" NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "CourseContent" ALTER COLUMN "status" SET DEFAULT 'PENDING';
12 changes: 9 additions & 3 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,13 @@ model CourseFolder {
model CourseContent {
id String @id @default(uuid())
name String
contentUrl String
courseFolderId String
courseFolder CourseFolder @relation(fields: [courseFolderId], references: [id])
isUploaded Boolean
status ContentStatus @default(PENDING)
@@unique([courseFolderId, name])
@@unique([courseFolderId, name]) // ensures the combination of courseFolderId & name are not repeated
}

model Purchases {
Expand All @@ -80,4 +79,11 @@ model Purchases {
courseId String
Course Courses @relation(fields: [courseId], references: [id])
}

enum ContentStatus {
PROCESSING
PROCESSED
PENDING
FAILED
}
2 changes: 1 addition & 1 deletion backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ app.use(cookieParser());
app.use(express.json());
app.use(
cors({
origin: "https://course-app.puneetnj.fun",
origin: "http://localhost:5174",
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
Expand Down
70 changes: 41 additions & 29 deletions backend/src/v1/routes/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ import {
import auth from "../middleware/auth";
import client from "../utils/prisma";
import { v4 as uuidv4 } from "uuid";
import { getPresignedUrl, deleteImageFromS3 } from "../utils/aws";
import {
getPresignedUrl,
deleteImageFromS3,
getPresignedUrlTemp,
presignedUrlVideo,
} from "../utils/aws";
import { isCourseCreator } from "../utils/lib";
import { ContentStatus } from "@prisma/client";

export const courseRouter = Router();

Expand Down Expand Up @@ -137,6 +143,7 @@ courseRouter.put("/", auth(["Admin"]), async (req, res) => {
}
});

// Todo: Delete the videos from s3 aswell
courseRouter.delete("/:courseId", auth(["Admin"]), async (req, res) => {
try {
const courseId = req.params.courseId;
Expand Down Expand Up @@ -217,7 +224,20 @@ courseRouter.get("/:courseId/:contentId", auth(["User"]), async (req, res) => {
where: { id: contentId },
});

res.json({ content });
if (!content) {
res.status(400).json({ msg: "Invalid content Id" });
return;
}

const objectKey = `${courseId}/${content.courseFolderId}/${contentId}/master.m3u8`;

const signedUrl = presignedUrlVideo(objectKey);

const folder = await client.courseFolder.findFirst({
where: { id: content.courseFolderId },
});

res.json({ content, folder, signedUrl });
} catch (err) {
console.log(err);

Expand Down Expand Up @@ -307,32 +327,21 @@ courseRouter.post("/postContent", auth(["Admin"]), async (req, res) => {
return;
}

const courseName = isCreator.course!.name.replace(/\s+/g, "");
const folderName = folder.name.replace(/\s+/g, "");
// Create signed url
// Create a db entry
const courseId = folder.courseId;
const contentId = uuidv4();

// already have content name

function sanitizeName(name: string) {
return name.replace(/[^a-zA-Z0-9-]/g, "-");
}
const objectKey = `${courseId}/${folderId}/${contentId}`;

// Sanitize all parts
const sanitizedCourseName = sanitizeName(courseName);
const sanitizedFolderName = sanitizeName(folderName);
const sanitizedContentName = sanitizeName(contentName);

const objectKey = `${sanitizedCourseName}/${sanitizedFolderName}/${sanitizedContentName}`;

const encodedObjectKey = encodeURIComponent(objectKey).replace(/%2F/g, "/");

const signedUrl = await getPresignedUrl(encodedObjectKey, "video/mp4");
const signedUrl = await getPresignedUrlTemp(objectKey, "video/mp4");

const content = await client.courseContent.create({
data: {
id: contentId,
name: contentName,
courseFolderId: folderId,
isUploaded: false,
contentUrl: `${process.env.CDN_LINK}/${encodedObjectKey}`,
status: ContentStatus.PROCESSING,
},
});

Expand All @@ -342,12 +351,13 @@ courseRouter.post("/postContent", auth(["Admin"]), async (req, res) => {
}
});

courseRouter.post(
// Todo: add some token auth
courseRouter.put(
"/contentUploaded/:contentId",
auth(["Admin"]),

async (req, res) => {
try {
const creatorId = res.locals.Admin.id;
// const creatorId = res.locals.Admin.id;
const contentId = req.params.contentId;

// need to check if the creator who is trying to update the courseContent is the actual creator of the course or not
Expand All @@ -358,22 +368,24 @@ courseRouter.post(
},
});

if (course?.courseFolder.course.creatorId !== creatorId) {
res.status(400).json({ msg: "You are not the creator of the course" });
return;
}
// if (course?.courseFolder.course.creatorId !== creatorId) {
// res.status(400).json({ msg: "You are not the creator of the course" });
// return;
// }

const content = await client.courseContent.update({
where: {
id: contentId,
},
data: {
isUploaded: true,
status: ContentStatus.PROCESSED,
},
});

res.json({ msg: "Course updated successfully" });
} catch (err) {
console.log(err);

res.status(500).json({ msg: "Internal Server Error" });
}
}
Expand Down
28 changes: 28 additions & 0 deletions backend/src/v1/utils/aws.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import AWS from "aws-sdk";
import { getSignedUrl } from "aws-cloudfront-sign";

let s3: AWS.S3;

const cfSigningParams = {
keypairId: process.env.CF_PUBLIC_KEY as string,
privateKeyString: process.env.CF_PRIVATE_KEY as string,
};

const initilizeAws = () => {
if (s3 instanceof AWS.S3) return s3;

Expand All @@ -26,6 +32,17 @@ export const getPresignedUrl = (objectKey: string, contentType: string) => {
});
};

export const getPresignedUrlTemp = (objectKey: string, contentType: string) => {
initilizeAws();

return s3.getSignedUrlPromise("putObject", {
Bucket: "puneet-course-app-temp",
Key: objectKey,
Expires: 240,
ContentType: contentType,
});
};

export const deleteImageFromS3 = (prevImageKey: string) => {
initilizeAws();

Expand All @@ -39,3 +56,14 @@ export const deleteImageFromS3 = (prevImageKey: string) => {
}
);
};

export const presignedUrlVideo = (objectKey: string) => {
initilizeAws();

const signedUrl = getSignedUrl(
process.env.CDN_LINK + `/${objectKey}`,
cfSigningParams
);

return signedUrl;
};
Loading

0 comments on commit a34162a

Please sign in to comment.