-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #209 from yuminn-k/feat/screen-sharing-enhancements
画面共有機能の強化とSwaggerドキュメントの更新
- Loading branch information
Showing
9 changed files
with
698 additions
and
526 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,193 +1,129 @@ | ||
package controllers | ||
|
||
import ( | ||
"fmt" | ||
"github.com/YJU-OKURA/project_minori-gin-deployment-repo/constants" | ||
"github.com/YJU-OKURA/project_minori-gin-deployment-repo/services" | ||
"github.com/dgrijalva/jwt-go" | ||
"github.com/gin-gonic/gin" | ||
"github.com/gorilla/websocket" | ||
"log" | ||
"os" | ||
"strings" | ||
"net/http" | ||
"strconv" | ||
) | ||
|
||
// LiveClassController handles all web socket operations for live classroom interactions | ||
// including creating a room, starting and stopping screen sharing | ||
type LiveClassController struct { | ||
liveClassService services.LiveClassService | ||
upgrader websocket.Upgrader | ||
Service services.LiveClassService | ||
} | ||
|
||
// RoomResponse encapsulates the response structure for room creation. | ||
type RoomResponse struct { | ||
RoomID string `json:"roomID"` | ||
} | ||
|
||
// ScreenShareResponse contains the SDP information necessary for establishing | ||
// a WebRTC connection for screen sharing. | ||
type ScreenShareResponse struct { | ||
SDP string `json:"sdp"` | ||
} | ||
|
||
// StandardResponse provides a generic response structure for simple messages. | ||
type StandardResponse struct { | ||
Message string `json:"message"` | ||
} | ||
|
||
// ErrorResponse provides a structured error message for API responses. | ||
type ErrorResponse struct { | ||
Error string `json:"error"` | ||
Details string `json:"details,omitempty"` | ||
} | ||
|
||
// NewLiveClassController creates a new controller instance with the necessary dependencies. | ||
func NewLiveClassController(service services.LiveClassService) *LiveClassController { | ||
return &LiveClassController{ | ||
liveClassService: service, | ||
upgrader: websocket.Upgrader{ | ||
ReadBufferSize: 1024, | ||
WriteBufferSize: 1024, | ||
}, | ||
Service: service, | ||
} | ||
} | ||
|
||
// CreateRoomHandler godoc | ||
// @Summary ルームを生成します。 | ||
// @Description ルームを生成します。 | ||
// CreateRoom godoc | ||
// @Summary 新しいルームを作成します。 | ||
// @Description 新しいルームを作成します。 | ||
// @Tags Live Class | ||
// @Accept json | ||
// @Produce json | ||
// @Success 200 {object} RoomResponse | ||
// @Failure 400 {object} ErrorResponse | ||
// @Router /live/create-room [post] | ||
func (c *LiveClassController) CreateRoomHandler() gin.HandlerFunc { | ||
return func(ctx *gin.Context) { | ||
roomID, err := c.liveClassService.CreateRoom() | ||
if err != nil { | ||
respondWithError(ctx, constants.StatusInternalServerError, "Failed to create room"+err.Error()) | ||
return | ||
} | ||
respondWithSuccess(ctx, constants.StatusOK, RoomResponse{RoomID: roomID}) | ||
// @Param classID path uint true "Class ID" | ||
// @Param userID path uint true "User ID" | ||
// @Success 200 {object} map[string]interface{} "roomID returned on successful creation" | ||
// @Failure 400 {object} map[string]interface{} "Invalid class ID" | ||
// @Failure 401 {object} map[string]interface{} "Unauthorized to create room" | ||
// @Failure 500 {object} map[string]interface{} "Internal server error" | ||
// @Router /live/create-room/{classID}/{userID} [post] | ||
func (c *LiveClassController) CreateRoom(ctx *gin.Context) { | ||
userID, _ := strconv.ParseUint(ctx.Param("userID"), 10, 32) | ||
classID, err := strconv.ParseUint(ctx.Param("classID"), 10, 32) | ||
if err != nil { | ||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid classID"}) | ||
return | ||
} | ||
|
||
roomID, err := c.Service.CreateRoom(uint(classID), uint(userID)) | ||
if err != nil { | ||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||
return | ||
} | ||
ctx.JSON(http.StatusOK, gin.H{"roomID": roomID}) | ||
} | ||
|
||
// StartScreenShareHandler godoc | ||
// StartScreenShare godoc | ||
// @Summary 画面共有を開始します。 | ||
// @Description 画面共有を開始します。 | ||
// @Tags Live Class | ||
// @Produce json | ||
// @Param Authorization header string true "Bearer Token" | ||
// @Param roomID path string true "ルームID" | ||
// @Param userID path string true "ユーザーID" | ||
// @Success 200 {object} ScreenShareResponse | ||
// @Failure 400 {object} ErrorResponse | ||
// @Router /live/start-screen-share/{roomID}/{userID} [get] | ||
func (c *LiveClassController) StartScreenShareHandler() gin.HandlerFunc { | ||
return func(ctx *gin.Context) { | ||
authHeader := ctx.GetHeader("Authorization") | ||
if !authenticateUser(authHeader) { | ||
respondWithError(ctx, constants.StatusUnauthorized, "Unauthorized access") | ||
return | ||
} | ||
|
||
roomID, userID := ctx.Param("roomID"), ctx.Param("userID") | ||
pc, err := c.liveClassService.StartScreenShare(roomID, userID) | ||
if err != nil { | ||
respondWithError(ctx, constants.StatusInternalServerError, "Screen sharing could not be started: "+err.Error()) | ||
return | ||
} | ||
|
||
offer, err := pc.CreateOffer(nil) | ||
if err != nil { | ||
pc.Close() | ||
respondWithError(ctx, constants.StatusInternalServerError, "Failed to create offer: "+err.Error()) | ||
return | ||
} | ||
|
||
if err := pc.SetLocalDescription(offer); err != nil { | ||
respondWithError(ctx, constants.StatusInternalServerError, "Failed to set local description: "+err.Error()) | ||
return | ||
} | ||
|
||
respondWithSuccess(ctx, constants.StatusOK, ScreenShareResponse{SDP: pc.LocalDescription().SDP}) | ||
log.Printf("Screen sharing started by user %s in room %s", userID, roomID) | ||
// @Accept json | ||
// @Produce json | ||
// @Param roomID path string true "Room ID" | ||
// @Param userID path uint true "User ID" | ||
// @Success 200 {object} map[string]interface{} "SDP data for the screen share" | ||
// @Failure 400 {object} map[string]interface{} "Invalid room ID" | ||
// @Failure 401 {object} map[string]interface{} "Unauthorized to start screen sharing" | ||
// @Failure 500 {object} map[string]interface{} "Internal server error" | ||
// @Router /live/start-screen-share/{roomID}/{userID} [post] | ||
func (c *LiveClassController) StartScreenShare(ctx *gin.Context) { | ||
roomID := ctx.Param("roomID") | ||
userID := ctx.GetString("userID") | ||
err := c.Service.StartScreenShare(roomID, userID) | ||
if err != nil { | ||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||
return | ||
} | ||
sdp, err := c.Service.GetScreenShareSDP(roomID) | ||
if err != nil { | ||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||
return | ||
} | ||
ctx.JSON(http.StatusOK, gin.H{"sdp": sdp}) | ||
} | ||
|
||
// StopScreenShareHandler godoc | ||
// StopScreenShare godoc | ||
// @Summary 画面共有を停止します。 | ||
// @Description 画面共有を停止します。 | ||
// @Tags Live Class | ||
// @Accept json | ||
// @Produce json | ||
// @Param Authorization header string true "Bearer Token" | ||
// @Param roomID path string true "ルームID" | ||
// @Param userID path string true "ユーザーID" | ||
// @Success 200 {object} StandardResponse | ||
// @Failure 400 {object} ErrorResponse | ||
// @Router /live/stop-screen-share/{roomID} [get] | ||
func (c *LiveClassController) StopScreenShareHandler() gin.HandlerFunc { | ||
return func(ctx *gin.Context) { | ||
roomID := ctx.Param("roomID") | ||
userID := ctx.Param("userID") | ||
err := c.liveClassService.StopScreenShare(roomID, userID) | ||
if err != nil { | ||
respondWithError(ctx, constants.StatusInternalServerError, "Screen sharing could not be stopped: "+err.Error()) | ||
return | ||
} | ||
respondWithSuccess(ctx, constants.StatusOK, StandardResponse{Message: "Screen share stopped successfully"}) | ||
// @Accept json | ||
// @Produce json | ||
// @Param roomID path string true "Room ID" | ||
// @Param userID path uint true "User ID" | ||
// @Success 200 {object} map[string]interface{} "Screen sharing stopped successfully" | ||
// @Failure 400 {object} map[string]interface{} "Invalid room ID" | ||
// @Failure 401 {object} map[string]interface{} "Unauthorized to stop screen sharing" | ||
// @Failure 500 {object} map[string]interface{} "Internal server error" | ||
// @Router /live/stop-screen-share/{roomID}/{userID} [post] | ||
func (c *LiveClassController) StopScreenShare(ctx *gin.Context) { | ||
roomID := ctx.Param("roomID") | ||
adminID := ctx.GetString("adminID") | ||
err := c.Service.StopScreenShare(roomID, adminID) | ||
if err != nil { | ||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||
return | ||
} | ||
ctx.JSON(http.StatusOK, gin.H{"message": "Screen sharing stopped"}) | ||
} | ||
|
||
// ViewScreenShareHandler godoc | ||
// @Summary 画面共有情報を取得します。 | ||
// @Description 画面共有情報を取得します。 | ||
// JoinScreenShare godoc | ||
// @Summary 画面共有に参加します。 | ||
// @Description 画面共有に参加します。 | ||
// @Tags Live Class | ||
// @Produce json | ||
// @Param Authorization header string true "Bearer Token" | ||
// @Success 200 {object} ScreenShareResponse "SDP information" | ||
// @Failure 400 {object} ErrorResponse "Error message and details" | ||
// @Failure 401 "Unauthorized if the user is not authenticated or not part of the room" | ||
// @Router /live/view-screen-share/{roomID} [get] | ||
func (c *LiveClassController) ViewScreenShareHandler() gin.HandlerFunc { | ||
return func(ctx *gin.Context) { | ||
roomID := ctx.Param("roomID") | ||
userID, exists := ctx.Get("userID") | ||
if !exists { | ||
respondWithError(ctx, constants.StatusUnauthorized, "User ID not provided") | ||
return | ||
} | ||
|
||
if !c.liveClassService.IsUserInRoom(userID.(string), roomID) { | ||
respondWithError(ctx, constants.StatusUnauthorized, "Access denied") | ||
return | ||
} | ||
|
||
sdp, err := c.liveClassService.GetScreenShareInfo(roomID) | ||
if err != nil { | ||
respondWithError(ctx, constants.StatusInternalServerError, "Failed to retrieve screen share info: "+err.Error()) | ||
return | ||
} | ||
|
||
respondWithSuccess(ctx, constants.StatusOK, ScreenShareResponse{SDP: sdp}) | ||
// @Accept json | ||
// @Produce json | ||
// @Param roomID path string true "Room ID" | ||
// @Param userID path uint true "User ID" | ||
// @Success 200 {object} map[string]interface{} "SDP data for the screen share" | ||
// @Failure 400 {object} map[string]interface{} "Invalid room ID or User ID" | ||
// @Failure 401 {object} map[string]interface{} "Unauthorized to join screen sharing" | ||
// @Failure 500 {object} map[string]interface{} "Internal server error" | ||
// @Router /live/join-screen-share/{roomID}/{userID} [get] | ||
func (c *LiveClassController) JoinScreenShare(ctx *gin.Context) { | ||
roomID := ctx.Param("roomID") | ||
userID, err := strconv.ParseUint(ctx.Param("userID"), 10, 32) | ||
if err != nil { | ||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid userID"}) | ||
return | ||
} | ||
} | ||
|
||
// authenticateUser checks if the provided JWT token is valid and authorized to access the system. | ||
func authenticateUser(tokenString string) bool { | ||
tokenString = strings.TrimPrefix(tokenString, "Bearer ") | ||
|
||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { | ||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | ||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) | ||
} | ||
return []byte(os.Getenv("JWT_SECRET")), nil | ||
}) | ||
|
||
offer, err := c.Service.JoinScreenShare(roomID, uint(userID)) | ||
if err != nil { | ||
log.Printf("Failed to authenticate user: %v", err) | ||
return false | ||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||
return | ||
} | ||
|
||
return token.Valid | ||
ctx.JSON(http.StatusOK, gin.H{"offer": offer}) | ||
} |
Oops, something went wrong.