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
16 changes: 16 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Build

on:
push:
branches: [main, "agent/**"]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- run: dotnet test tests/V380Decoder.Tests/V380Decoder.Tests.csproj --configuration Release
7 changes: 4 additions & 3 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version (e.g., v1.0.0)'
description: 'Version (e.g., v1.0.4)'
required: true
default: 'v1.0.0'

jobs:
build:
Expand Down Expand Up @@ -120,6 +119,8 @@ jobs:
name: Create Release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout
Expand Down Expand Up @@ -173,7 +174,7 @@ jobs:
EOF

- name: Create release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
name: ${{ steps.version.outputs.tag }}
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ RUN dotnet publish V380Decoder.csproj -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /app .
EXPOSE 8554/tcp 8080/tcp
ENTRYPOINT ["./V380Decoder"]
ENTRYPOINT ["./V380Decoder"]
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ This is a port of [prsyahmi/v380](https://github.com/prsyahmi/v380) with signifi
- ✅ Web UI and REST API for camera control
- ✅ Snapshot API
- ✅ Cloud relay streaming support
- ✅ H.265/HEVC streams (`0x28` keyframes and `0x29` inter frames)
- ✅ Automatic media decryption layout detection for protocol versions 20/21

## Tested Cameras

**Note:** I've only tested with 2 V380 cameras running device version 31 with H264 stream.
H.264 device-version-31 cameras and encrypted H.265 device-version-32
cloud streams are supported. V380 is a protocol family rather than one fixed
camera format, so logs from additional models are welcome.

**Camera 1:**
- Software: `AppEV2W_VA3_V2.5.9.5_20231211`
Expand All @@ -25,7 +29,7 @@ This is a port of [prsyahmi/v380](https://github.com/prsyahmi/v380) with signifi
## Requirements

- .NET 10 SDK (for building from source)
- FFmpeg (optional, for snapshot or piping video/audio output)
- FFmpeg (required for H.265 snapshots/MJPEG; optional for H.264-only use)

## Command Line Arguments

Expand Down Expand Up @@ -58,7 +62,11 @@ Download Latest [Release](https://github.com/PyanSofyan/V380decoder/releases/lat
```
### Video Output (pipe to FFplay)
```bash
# H.264 camera
./V380Decoder --id 12345678 --username admin --password password --ip 192.168.1.2 --output video | ffplay -f h264 -i pipe:0

# H.265 camera
./V380Decoder --id 12345678 --username admin --password password --source cloud --output video | ffplay -f hevc -i pipe:0
```

### Audio Output (pipe to FFplay)
Expand Down Expand Up @@ -91,6 +99,18 @@ Streaming via relay server (relay IP automatically detected):
./V380Decoder --id 12345678 --username admin --password password --source cloud
```

For newer encrypted cameras, startup logs should report the detected codec and
decryption layout, for example:

```text
[VIDEO] codec=H265 decrypt=FullBlocks header=16
[RTSP] video codec=H265
```

If the media protocol version is absent from a relay response, the decoder
validates Annex-B NAL units and automatically chooses between the version-20
selective `64/80` AES layout and version-21 full-block AES.

## ONVIF Support

Tested with [Onvif Device Manager](https://sourceforge.net/projects/onvifdm/) (ODM), [Onvif Integration](https://www.home-assistant.io/integrations/onvif/) Home Assistant and [Shinobi](https://shinobi.video/)
Expand Down Expand Up @@ -243,6 +263,9 @@ docker build -t v380decoder .
docker run -d --restart unless-stopped --network host v380decoder --id 12345678 --username admin --password password --ip 192.168.1.2 --enable-onvif --enable-api
```

The Docker image includes FFmpeg, so H.265 snapshots and MJPEG work without an
additional package installation.

## Acknowledgements

- [prsyahmi/v380](https://github.com/prsyahmi/v380) - Original V380 reverse engineering work
Expand Down
5 changes: 4 additions & 1 deletion V380Decoder.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@
<PackageReference Include="H264Sharp" Version="1.6.0"/>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12"/>
</ItemGroup>
</Project>
<ItemGroup>
<Compile Remove="tests/**/*.cs"/>
</ItemGroup>
</Project>
149 changes: 149 additions & 0 deletions src/AnnexB.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
namespace V380Decoder.src
{
internal static class AnnexB
{
public static bool TryNormalize(byte[] data, out byte[] normalized)
{
int start = FindStartCode(data, 0);
if (start < 0 || start > 128)
{
normalized = Array.Empty<byte>();
return false;
}

int startCodeLength = GetStartCodeLength(data, start);
if (startCodeLength == 4 && start == 0)
{
normalized = data;
return true;
}

int payloadLength = data.Length - start - startCodeLength;
if (payloadLength <= 0)
{
normalized = Array.Empty<byte>();
return false;
}

normalized = new byte[4 + payloadLength];
normalized[3] = 1;
Array.Copy(data, start + startCodeLength, normalized, 4, payloadLength);
return true;
}

public static void Parse(byte[] data, VideoCodec codec, Action<int, byte[]> callback)
{
int offset = 0;
while (offset < data.Length)
{
int start = FindStartCode(data, offset);
if (start < 0) break;

int nalStart = start + GetStartCodeLength(data, start);
if (nalStart >= data.Length) break;

int next = FindStartCode(data, nalStart);
int nalEnd = next < 0 ? data.Length : next;
int nalLength = nalEnd - nalStart;
if (nalLength <= 0)
{
offset = nalStart;
continue;
}

if (codec == VideoCodec.H265 && nalLength < 2)
{
offset = nalEnd;
continue;
}

int nalType = codec == VideoCodec.H265
? (data[nalStart] >> 1) & 0x3F
: data[nalStart] & 0x1F;

var nal = new byte[nalLength];
Array.Copy(data, nalStart, nal, 0, nalLength);
callback(nalType, nal);
offset = nalEnd;
}
}

public static int Score(byte[] data, VideoCodec codec)
{
int score = 0;
int count = 0;
bool invalid = false;

Parse(data, codec, (type, nal) =>
{
count++;

if (codec == VideoCodec.H264)
{
if (type is <= 0 or >= 24)
{
invalid = true;
return;
}

score += type switch
{
7 => 60,
8 => 50,
5 => 40,
_ => 8
};
return;
}

bool validHeader = nal.Length >= 2 &&
(nal[0] & 0x80) == 0 &&
(nal[1] & 0x07) != 0;
if (!validHeader)
{
invalid = true;
return;
}

score += type switch
{
32 => 60, // VPS
33 => HevcSpsParser.TryGetDimensions(nal, out _, out _)
? 270
: MarkInvalid(),
34 => 60, // PPS
>= 16 and <= 23 => 45, // IRAP/IDR/CRA
<= 31 => 10, // VCL
<= 47 => 5, // common non-VCL
_ => 1
};

int MarkInvalid()
{
invalid = true;
return 0;
}
});

if (count == 0 || invalid) return int.MinValue;
return score + Math.Min(count, 12) * 2;
}

public static int FindStartCode(byte[] data, int from)
{
for (int i = Math.Max(from, 0); i + 2 < data.Length; i++)
{
if (data[i] != 0 || data[i + 1] != 0) continue;
if (data[i + 2] == 1) return i;
if (i + 3 < data.Length && data[i + 2] == 0 && data[i + 3] == 1)
return i;
}
return -1;
}

private static int GetStartCodeLength(byte[] data, int offset)
{
return offset + 2 < data.Length && data[offset + 2] == 1 ? 3 : 4;
}
}
}
3 changes: 3 additions & 0 deletions src/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("V380Decoder.Tests")]
3 changes: 2 additions & 1 deletion src/Enum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ namespace V380Decoder.src
{
public enum OutputMode { Video, Audio, Rtsp }
public enum SourceStream { Lan, Cloud }
}
public enum VideoCodec { H264, H265 }
}
7 changes: 4 additions & 3 deletions src/FrameData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ namespace V380Decoder.src
{
public class FrameData
{
public byte RawType; // fragment header type byte (0x00/0x01/0x1A)
public byte RawType;
public uint FrameId;
public ushort FrameType;
public ushort FrameRate;
public ulong Timestamp;
public VideoCodec Codec = VideoCodec.H264;
public byte[] Payload;
public bool IsKeyframe => RawType == 0x00;
public bool IsKeyframe => RawType is 0x00 or 0x28;
}
}
}
Loading
Loading