Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/android-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Build Android APK and Video Server

on:
push:
branches:
- '**'

jobs:
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '22'

- name: Cache Android Gradle
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
Android/.gradle
key: ${{ runner.os }}-android-gradle-${{ hashFiles('Android/gradlew', 'Android/build.gradle', 'Android/app/build.gradle') }}

- name: Build APK
run: chmod +x gradlew && ./gradlew assembleDebug
working-directory: ./Android

- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: debug-apk
path: Android/app/build/outputs/apk/debug/app-debug.apk

build-videoserver:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '22'

- name: Cache VideoServer Gradle
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
VideoServer/.gradle
key: ${{ runner.os }}-videoserver-gradle-${{ hashFiles('VideoServer/gradlew', 'VideoServer/build.gradle') }}

- name: Build VideoServer Shadow JAR
run: chmod +x gradlew && ./gradlew shadowJar
working-directory: ./VideoServer

- name: Upload VideoServer shadow JAR artifact
uses: actions/upload-artifact@v4
with:
name: video-server-shadow-jar
path: VideoServer/build/libs/*-SNAPSHOT.jar
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ class SettingsFragment : Fragment() {
activity?.onBackPressed()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ class SettingsPreferences(context: Context) {
}

fun getIpAddress() : String? {
return sharedPreferences.getString(IP_KEY, "192.168.0.101:4321")
return sharedPreferences.getString(IP_KEY, "192.168.178.101:4321")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import android.util.Range
import android.view.SurfaceHolder
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.updateLayoutParams
import com.ipcamera.databinding.StreamActivityBinding
import java.io.DataOutputStream
import java.net.Socket
Expand All @@ -39,31 +37,10 @@ class StreamActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

EdgeToEdge.setDecorFitsSystemWindows(
window = window,
fitSystemWindows = false,
)

EdgeToEdge.enableImmersiveMode(window = window)

binding = StreamActivityBinding.inflate(layoutInflater)

setContentView(binding.root)

EdgeToEdge.setInsetsHandler(
root = binding.root,
handler = StreamActivityInsetsHandler { systemBarInsets ->

binding.btnSave.updateLayoutParams<ConstraintLayout.LayoutParams> {
bottomMargin += systemBarInsets.bottom
}

binding.tvStatus.updateLayoutParams<ConstraintLayout.LayoutParams> {
topMargin += systemBarInsets.top
}
}
)

val cameraManager = getSystemService(CameraManager::class.java)

val cameraId = cameraManager.cameraIdList[0]
Expand Down Expand Up @@ -104,6 +81,8 @@ class StreamActivity : AppCompatActivity() {
val port = ipAddress.split(":")[1]

socket = Socket(ip, port.toInt())
socket?.sendBufferSize = 900000000
socket?.receiveBufferSize = 900000000

mainHandler.post {
binding.tvStatus.text = "Streaming to: $ipAddress"
Expand All @@ -127,8 +106,19 @@ class StreamActivity : AppCompatActivity() {

Log.d(TAG, "Buffer size: ${queue.size}")
start = System.currentTimeMillis()
size = frame.size

while (size > 0) {
stack.addLast(size % 10)
size /= 10
}

socketWriter.writeByte(stack.size)

while (stack.isNotEmpty()) {
socketWriter.writeByte(stack.removeLast())
}

socketWriter.writeInt(frame.size)
socketWriter.write(frame)

socketWriter.flush()
Expand Down Expand Up @@ -174,17 +164,17 @@ class StreamActivity : AppCompatActivity() {
}

val buffer = image.planes[0].buffer
buffer.rewind()

if (buffer.hasArray()) {
queue.add(buffer.array())
} else {
val array = ByteArray(buffer.remaining())
buffer.get(array)
val arr = ByteArray(buffer.capacity())

queue.add(array)
var i = 0
while (buffer.hasRemaining()) {
arr[i++] = buffer.get()
}

image.close()
queue.add(arr)
}
}, null)

Expand All @@ -205,9 +195,10 @@ class StreamActivity : AppCompatActivity() {
override fun onCaptureProgressed(
session: CameraCaptureSession,
request: CaptureRequest,
partialResult: CaptureResult,
partialResult: CaptureResult
) {
super.onCaptureProgressed(session, request, partialResult)
Log.d(TAG, "onCaptureProgressed: ")
}
}

Expand Down Expand Up @@ -263,4 +254,4 @@ class StreamActivity : AppCompatActivity() {

})
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:helperText="Format: 192.168.101:4321"
app:helperText="Example: 192.168.168.x:4321"
android:layout_marginHorizontal="@dimen/horizontal_margin"
>

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_text_ip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Server IP Address"
android:hint="Enter server IP address"
android:inputType="textNoSuggestions"
/>

Expand All @@ -46,4 +46,4 @@
android:text="Save"
/>

</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
File renamed without changes.
File renamed without changes.
File renamed without changes.
92 changes: 52 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,67 @@
# IP Camera
![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/preview.gif?raw=true)

[Fullscreen](https://youtu.be/NtQ_Al-56Qs)
[![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/preview.gif?raw=true)](https://youtu.be/NtQ_Al-56Qs)

## Overview

![Overview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/high_level_overview.png?raw=true)

## How to use
You can either watch this video or follow the steps below.
### How to start live streaming
1. Start the Video server. By default the Video server launches 3 sockets, each acting as a server:
- WebSocket Server (runs on port 1234).
- MJPEG Server (runs on port 4444).
- Camera Server (runs on port 4321).
## How to Use

You can either watch the video above or follow the steps below.

### How to Start Live Streaming

1. Start the Video Server. By default, the Video Server launches 3 sockets, each acting as a server:
- WebSocket Server (runs on port 1234)
- MJPEG Server (runs on port 4444)
- Camera Server (runs on port 4321)
2. Install the app on your phone.
3. Navigate to app's settings screen and setup your Camera's server IP. For example `192.168.0.101:4321`.
4. Open the stream screen and click the Start streaming button.
5. Now your phone sends video data to your Camera Server.
3. Navigate to the app's settings screen and set up your camera server's IP. For example: `192.168.178.101:4321`
4. Open the stream screen and click the "Start streaming" button.
5. Your phone is now sending video data to your Camera Server.

---
### Watching the stream
The stream can be watched from either your browser, the Web App or apps like VLC media player.

### Watching the Stream

The stream can be watched from either your browser, the Web App, or apps like VLC Media Player.

### Browser
Open your favorite web browser and navigate to your MJPEG server's IP address. For example `http://192.168.0.101:4444`

![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/browser.gif?raw=true)
Open your favorite web browser and navigate to your MJPEG Server's IP address. For example:
`http://192.168.178.101:4444`

[![Browser Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/browser.gif?raw=true)](https://youtu.be/NtQ_Al-56Qs)

### VLC Media Player

Open VLC Media Player. Go to **File → Open Network → Network** and enter your MJPEG Server's IP address. For example:
`http://192.168.178.101:4444/`

### VLC meida player
Open the VLC media player, File -> Open Network -> Network and write your MJPEG's server IP address. For example `http://192.168.0.101:4444/`
[![VLC Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/vlc.gif?raw=true)](https://youtu.be/NtQ_Al-56Qs)

![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/vlc.gif?raw=true)
### The Web App
1. Navigate to the Web app root directory and in your terminal execute `webpack serve`.
2. Open your browser and navigate to `http://localhost:8080/`.
3. Go to settings and enter your WebSocket server ip address. For example `192.168.0.101:1234`.
4. Go to the streaming page `http://localhost:8080/stream.html` and click the connect button.

![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/webapp.gif?raw=true)

### Configuring the Web App's server
Note: This section is required only if you'd like to be able to take screenshots from the Web App.

1. Open the Web App Server project
2. Open index.js and edit the connection object to match your MySQL credentials.
3. Create the required tables by executing the SQL query located in `user.sql`
4. At the root directory execute `node index.js` in your terminal
5. You may have to update the IP that the Web App connects to. You can edit this IP in Web app's `stream.html` file (`BACKEND_URL` const variable)
6. Create a user through the Web App from `http://localhost:8080/register.html`
7. Take screenshots from `http://localhost:8080/stream.html`
8. View your screenshots at `http://localhost:8080/gallery.html`

![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/webapp_gallery.gif?raw=true)
---

1. Navigate to the Web App's root directory and execute `webpack serve` in your terminal.
2. Open your browser and go to `http://localhost:8080/`.
3. Go to the settings page and enter your WebSocket Server's IP address. For example: `192.168.178.101:1234`
4. Navigate to the streaming page at `http://localhost:8080/stream.html` and click the "Connect" button.

[![Web App Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/webapp.gif?raw=true)](https://youtu.be/NtQ_Al-56Qs)

### Configuring the Web App's Server

> **Note:** This section is only required if you'd like to take screenshots from the Web App.

1. Open the Web App's server project.
2. Open `index.js` and edit the connection object to match your MySQL credentials.
3. Create the required tables by executing the SQL query located in `user.sql`.
4. From the root directory, run `node index.js` in your terminal.
5. You may have to update the IP that the Web App connects to. You can edit this IP in the Web App's `stream.html` file (see the `BACKEND_URL` constant).
6. Create a user through the Web App at `http://localhost:8080/register.html`.
7. Take screenshots from `http://localhost:8080/stream.html`.
8. View your screenshots at `http://localhost:8080/gallery.html`.

[![Web App Gallery Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/webapp_gallery.gif?raw=true)](https://youtu.be/NtQ_Al-56Qs)

23 changes: 23 additions & 0 deletions VideoServer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM eclipse-temurin:22-jdk AS build

WORKDIR /app

COPY gradlew gradlew.bat settings.gradle build.gradle /app/
COPY gradle /app/gradle

COPY src /app/src

RUN chmod +x gradlew
RUN ./gradlew clean shadowJar

FROM eclipse-temurin:22-jdk

WORKDIR /app

COPY --from=build /app/build/libs/VideoServer-1.0-SNAPSHOT.jar /app/VideoServer.jar

ENTRYPOINT ["java", "-jar", "/app/VideoServer.jar"]

EXPOSE 1234
EXPOSE 4444
EXPOSE 4321
Loading