Skip to content

Instantly share code, notes, and snippets.

@flushpot1125
Created May 18, 2025 06:12
Show Gist options
  • Select an option

  • Save flushpot1125/7ef33aba0857168292cef7b7beada5b5 to your computer and use it in GitHub Desktop.

Select an option

Save flushpot1125/7ef33aba0857168292cef7b7beada5b5 to your computer and use it in GitHub Desktop.
package com.example.videostream
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.server.response.respondText
import io.ktor.websocket.WebSocketSession
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.net.Inet4Address
import java.net.NetworkInterface
import io.ktor.server.cio.*
import io.ktor.server.response.respond
class MainActivity : ComponentActivity() {
private var server: ApplicationEngine? = null
private var isServerRunning by mutableStateOf(false)
private var isStreamingVideo by mutableStateOf(false)
// カメラ権限のリクエスト
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
Log.d("Camera", "Permission granted")
} else {
Log.d("Camera", "Permission denied")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// カメラ権限を確認
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED) {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
enableEdgeToEdge()
setContent {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
if (!isServerRunning) {
Button(
onClick = { startServer() },
modifier = Modifier.padding(8.dp)
) {
Text("サーバー開始 & 映像伝送開始")
}
} else {
Button(
onClick = { stopServer() },
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
modifier = Modifier.padding(8.dp)
) {
Text("映像伝送停止")
}
Spacer(modifier = Modifier.height(16.dp))
Text("映像配信中: http://${getLocalIpAddress()}:8080")
Text("ブラウザからアクセスしてください")
}
}
}
}
}
private fun startServer() {
lifecycleScope.launch(Dispatchers.IO) {
try {
// Androidで動作するように設定を調整
val environment = applicationEngineEnvironment {
module {
install(WebSockets)
routing {
// index.htmlのルーティング(既存のもの)
get("/") {
val indexHtml = try {
assets.open("static/index.html").bufferedReader().use { it.readText() }
} catch (e: Exception) {
Log.e("KtorServer", "Error loading index.html", e)
"HTML file not found"
}
call.respondText(indexHtml, ContentType.Text.Html)
}
// client.jsのルーティングを明示的に追加
get("/client.js") {
try {
val jsContent = assets.open("static/client.js").bufferedReader().use { it.readText() }
call.respondText(jsContent, ContentType.Text.JavaScript)
} catch (e: Exception) {
Log.e("KtorServer", "Error loading client.js", e)
call.respond(HttpStatusCode.NotFound)
}
}
webSocket("/video") {
startVideoStream(this)
}
}
}
// サーバーの設定
connector {
host = "0.0.0.0"
port = 8080
}
}
// server = embeddedServer(Netty, environment).start(wait = false)
server = embeddedServer(CIO, environment).start(wait = false)
isServerRunning = true
isStreamingVideo = true
Log.d("KtorServer", "Server started on port 8080")
} catch (e: Exception) {
Log.e("KtorServer", "Error starting server", e)
}
}
}
private fun stopServer() {
lifecycleScope.launch(Dispatchers.IO) {
isStreamingVideo = false
server?.stop(1000, 2000)
server = null
isServerRunning = false
Log.d("KtorServer", "Server stopped")
}
}
private suspend fun startVideoStream(session: WebSocketSession) {
val cameraCapture = CameraCapture(this)
try {
cameraCapture.startCamera(session)
// セッションが閉じるまで待機
for (frame in session.incoming) {
// クライアントからのメッセージを処理(必要に応じて)
}
} catch (e: Exception) {
Log.e("KtorServer", "Error in video stream", e)
} finally {
cameraCapture.stopCamera()
}
}
private fun getLocalIpAddress(): String {
try {
val en = NetworkInterface.getNetworkInterfaces()
while (en.hasMoreElements()) {
val intf = en.nextElement()
Log.d("Network", "Interface: ${intf.displayName}")
val enumIpAddr = intf.inetAddresses
while (enumIpAddr.hasMoreElements()) {
val inetAddress = enumIpAddr.nextElement()
Log.d("Network", "IP: ${inetAddress.hostAddress}, Loopback: ${inetAddress.isLoopbackAddress}")
if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) {
return inetAddress.hostAddress.toString()
}
}
}
} catch (e: Exception) {
Log.e("KtorServer", "Error getting IP address", e)
}
return "Unknown IP"
}
override fun onDestroy() {
stopServer()
super.onDestroy()
}
}
@Composable
fun ServerStatus(ipAddress: String) {
Text("Ktor server running at: http://$ipAddress:8080")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment