chore: 发布 v1.0.1 并支持端口参数

This commit is contained in:
2026-06-23 17:01:25 +08:00
parent ae8fe7f31b
commit ebded5057f
4 changed files with 75 additions and 34 deletions

View File

@@ -1,7 +1,10 @@
package main package main
import ( import (
"flag"
"fmt"
"net/http" "net/http"
"os"
"time" "time"
"hightube/internal/api" "hightube/internal/api"
@@ -11,9 +14,16 @@ import (
"hightube/internal/stream" "hightube/internal/stream"
) )
type serverConfig struct {
apiPort int
rtmpPort int
}
func main() { func main() {
cfg := parseFlags()
monitor.Init(2000) monitor.Init(2000)
monitor.Infof("Starting Hightube Server v1.0.0-Beta4.8") monitor.Infof("Starting Hightube Server v1.0.1")
// Initialize Database and run auto-migrations // Initialize Database and run auto-migrations
db.InitDB() db.InitDB()
@@ -21,28 +31,52 @@ func main() {
// Initialize Chat WebSocket Hub // Initialize Chat WebSocket Hub
chat.InitChat() chat.InitChat()
srv := stream.NewRTMPServer() srv := stream.NewRTMPServer(fmt.Sprintf("%d", cfg.rtmpPort))
// Start the API server in a goroutine so it doesn't block the RTMP server // Start the API server in a goroutine so it doesn't block the RTMP server
go func() { go func() {
apiAddr := fmt.Sprintf(":%d", cfg.apiPort)
r := api.SetupRouter(srv) r := api.SetupRouter(srv)
httpServer := &http.Server{ httpServer := &http.Server{
Addr: ":8080", Addr: apiAddr,
Handler: r, Handler: r,
ReadHeaderTimeout: 5 * time.Second, ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20, MaxHeaderBytes: 1 << 20,
} }
monitor.Infof("API server listening on :8080") monitor.Infof("API server listening on %s", apiAddr)
monitor.Infof("Web console listening on :8080/admin") monitor.Infof("Web console listening on %s/admin", apiAddr)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
monitor.Errorf("Failed to start API server: %v", err) monitor.Errorf("Failed to start API server: %v", err)
} }
}() }()
// Setup and start the RTMP server // Setup and start the RTMP server
rtmpAddr := fmt.Sprintf(":%d", cfg.rtmpPort)
monitor.Infof("Ready to receive RTMP streams from OBS") monitor.Infof("Ready to receive RTMP streams from OBS")
if err := srv.Start(":1935"); err != nil { if err := srv.Start(rtmpAddr); err != nil {
monitor.Errorf("Failed to start RTMP server: %v", err) monitor.Errorf("Failed to start RTMP server: %v", err)
} }
} }
func parseFlags() serverConfig {
cfg := serverConfig{}
flag.IntVar(&cfg.apiPort, "api-port", 8080, "API/Web console listen port")
flag.IntVar(&cfg.rtmpPort, "rtmp-port", 1935, "RTMP listen port")
flag.Parse()
if !validPort(cfg.apiPort) {
fmt.Fprintf(os.Stderr, "invalid --api-port %d: port must be between 1 and 65535\n", cfg.apiPort)
os.Exit(2)
}
if !validPort(cfg.rtmpPort) {
fmt.Fprintf(os.Stderr, "invalid --rtmp-port %d: port must be between 1 and 65535\n", cfg.rtmpPort)
os.Exit(2)
}
return cfg
}
func validPort(port int) bool {
return port >= 1 && port <= 65535
}

View File

@@ -40,6 +40,7 @@ type RTMPServer struct {
transcoders map[string][]*variantTranscoder transcoders map[string][]*variantTranscoder
thumbnailJobs map[string]context.CancelFunc thumbnailJobs map[string]context.CancelFunc
internalPublishKey string internalPublishKey string
rtmpPort string
thumbnailDir string thumbnailDir string
mutex sync.RWMutex mutex sync.RWMutex
} }
@@ -100,13 +101,14 @@ func (w *bufferedWriteFlusher) Flush() error {
return nil return nil
} }
// NewRTMPServer creates and initializes a new media server // NewRTMPServer creates and initializes a new media server.
func NewRTMPServer() *RTMPServer { func NewRTMPServer(rtmpPort string) *RTMPServer {
s := &RTMPServer{ s := &RTMPServer{
channels: make(map[string]*pubsub.Queue), channels: make(map[string]*pubsub.Queue),
transcoders: make(map[string][]*variantTranscoder), transcoders: make(map[string][]*variantTranscoder),
thumbnailJobs: make(map[string]context.CancelFunc), thumbnailJobs: make(map[string]context.CancelFunc),
internalPublishKey: generateInternalPublishKey(), internalPublishKey: generateInternalPublishKey(),
rtmpPort: rtmpPort,
thumbnailDir: filepath.Join(os.TempDir(), "hightube-thumbnails"), thumbnailDir: filepath.Join(os.TempDir(), "hightube-thumbnails"),
server: &rtmp.Server{}, server: &rtmp.Server{},
} }
@@ -348,8 +350,8 @@ func (s *RTMPServer) startVariantTranscoders(roomID string) {
launch := make([]*variantTranscoder, 0, len(supportedQualities)) launch := make([]*variantTranscoder, 0, len(supportedQualities))
for quality, profile := range supportedQualities { for quality, profile := range supportedQualities {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
inputURL := fmt.Sprintf("rtmp://127.0.0.1:1935/live/%s", roomID) inputURL := fmt.Sprintf("rtmp://127.0.0.1:%s/live/%s", s.rtmpPort, roomID)
outputURL := fmt.Sprintf("rtmp://127.0.0.1:1935/variant/%s/%s/%s", roomID, quality, s.internalPublishKey) outputURL := fmt.Sprintf("rtmp://127.0.0.1:%s/variant/%s/%s/%s", s.rtmpPort, roomID, quality, s.internalPublishKey)
cmd := exec.CommandContext( cmd := exec.CommandContext(
ctx, ctx,
"ffmpeg", "ffmpeg",
@@ -475,7 +477,7 @@ func (s *RTMPServer) captureThumbnail(roomID string) {
"-y", "-y",
"-loglevel", "error", "-loglevel", "error",
"-rtmp_live", "live", "-rtmp_live", "live",
"-i", fmt.Sprintf("rtmp://127.0.0.1:1935/live/%s", roomID), "-i", fmt.Sprintf("rtmp://127.0.0.1:%s/live/%s", s.rtmpPort, roomID),
"-frames:v", "1", "-frames:v", "1",
"-q:v", "4", "-q:v", "4",
tempPath, tempPath,

View File

@@ -124,7 +124,10 @@ class _SettingsPageState extends State<SettingsPage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(l10n.settings, style: TextStyle(fontWeight: FontWeight.bold)), title: Text(
l10n.settings,
style: TextStyle(fontWeight: FontWeight.bold),
),
centerTitle: true, centerTitle: true,
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
@@ -142,11 +145,15 @@ class _SettingsPageState extends State<SettingsPage> {
DropdownButtonFormField<Locale?>( DropdownButtonFormField<Locale?>(
initialValue: settings.locale == null initialValue: settings.locale == null
? null ? null
: AppLocalizations.supportedLocales.cast<Locale?>().firstWhere( : AppLocalizations.supportedLocales
(l) => l?.languageCode == settings.locale?.languageCode && .cast<Locale?>()
l?.scriptCode == settings.locale?.scriptCode, .firstWhere(
orElse: () => null, (l) =>
), l?.languageCode ==
settings.locale?.languageCode &&
l?.scriptCode == settings.locale?.scriptCode,
orElse: () => null,
),
decoration: InputDecoration( decoration: InputDecoration(
labelText: l10n.selectLanguage, labelText: l10n.selectLanguage,
prefixIcon: const Icon(Icons.language), prefixIcon: const Icon(Icons.language),
@@ -155,10 +162,7 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
), ),
items: [ items: [
DropdownMenuItem( DropdownMenuItem(value: null, child: Text(l10n.system)),
value: null,
child: Text(l10n.system),
),
DropdownMenuItem( DropdownMenuItem(
value: const Locale('en'), value: const Locale('en'),
child: Text(l10n.english), child: Text(l10n.english),
@@ -168,7 +172,10 @@ class _SettingsPageState extends State<SettingsPage> {
child: Text(l10n.simplifiedChinese), child: Text(l10n.simplifiedChinese),
), ),
DropdownMenuItem( DropdownMenuItem(
value: const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), value: const Locale.fromSubtags(
languageCode: 'zh',
scriptCode: 'Hant',
),
child: Text(l10n.traditionalChinese), child: Text(l10n.traditionalChinese),
), ),
DropdownMenuItem( DropdownMenuItem(
@@ -258,7 +265,10 @@ class _SettingsPageState extends State<SettingsPage> {
}, },
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text(l10n.accentColor, style: Theme.of(context).textTheme.labelLarge), Text(
l10n.accentColor,
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 12), const SizedBox(height: 12),
Wrap( Wrap(
spacing: 12, spacing: 12,
@@ -301,9 +311,7 @@ class _SettingsPageState extends State<SettingsPage> {
SwitchListTile.adaptive( SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(l10n.livePreviewThumbnails), title: Text(l10n.livePreviewThumbnails),
subtitle: Text( subtitle: Text(l10n.livePreviewThumbnailsDesc),
l10n.livePreviewThumbnailsDesc,
),
value: settings.livePreviewThumbnailsEnabled, value: settings.livePreviewThumbnailsEnabled,
onChanged: settings.setLivePreviewThumbnailsEnabled, onChanged: settings.setLivePreviewThumbnailsEnabled,
), ),
@@ -392,10 +400,7 @@ class _SettingsPageState extends State<SettingsPage> {
"Hightube", "Hightube",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
), ),
Text( Text("Version: 1.0.1", style: TextStyle(color: Colors.grey)),
"Version: 1.0.0-beta4.8",
style: TextStyle(color: Colors.grey),
),
Text( Text(
"Author: Highground-Soft", "Author: Highground-Soft",
style: TextStyle(color: Colors.grey), style: TextStyle(color: Colors.grey),

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.0-beta4.8 version: 1.0.1
environment: environment:
sdk: ^3.11.1 sdk: ^3.11.1