Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true

[*]
charset = utf-8
end_of_line = crlf
indent_style = space

[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct,json,yml}]
Expand All @@ -11,9 +13,7 @@ trim_trailing_whitespace = true

# Code files
[*.{cs,csx,vb,vbx,xaml}]
charset = utf-8-bom
indent_size = 4
end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true

Expand Down
10 changes: 6 additions & 4 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
* text=auto eol=crlf

###############################################################################
# Set default behavior for command prompt diff.
Expand Down Expand Up @@ -40,9 +40,11 @@
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
*.ico binary
*.mp4 binary
#*.jpg binary
#*.png binary
#*.gif binary

###############################################################################
# diff behavior for common document formats
Expand Down
12 changes: 6 additions & 6 deletions Credits.txt
Original file line number Diff line number Diff line change
Expand Up @@ -490,16 +490,16 @@ This work of art is distributed under the following license:
Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0) which basically means that

You are free to:
Share copy and redistribute the material in any medium or format
Adapt remix, transform, and build upon the material
Share — copy and redistribute the material in any medium or format
Adapt — remix, transform, and build upon the material
The licensor cannot revoke these freedoms as long as you follow the license terms.

Under the following terms:
Attribution You must give appropriate credit, provide a link to the license, and indicate if
Attribution — You must give appropriate credit, provide a link to the license, and indicate if
changes were made. You may do so in any reasonable manner, but not in any way that suggests the
licensor endorses you or your use.
NonCommercial You may not use the material for commercial purposes.
ShareAlike If you remix, transform, or build upon the material, you must distribute your
NonCommercial — You may not use the material for commercial purposes.
ShareAlike — If you remix, transform, or build upon the material, you must distribute your
contributions under the same license as the original.

Please see the full license here: https://creativecommons.org/licenses/by-nc-sa/3.0/legalcode
Expand Down Expand Up @@ -943,4 +943,4 @@ https://github.com/Microsoft/msbuild/blob/ab090d1255caa87e742cbdbc6d7fe904ecebd9
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
30 changes: 14 additions & 16 deletions SentryReplay.Data/CamChunk.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
namespace SentryReplay;

/// <summary>
/// All the dashcam clips that were recorded at the same time from different angles.
/// Multiple of these can be grouped into a <see cref="CamClip"/>.
/// Synchronized camera files recorded at the same timestamp.
/// </summary>
public record class CamChunk
{
/// <summary>
/// Timestamp shared by the files in this chunk.
/// </summary>
public DateTime Timestamp { get; private init; }

/// <summary>
/// Files keyed by Tesla camera name.
/// </summary>
public IReadOnlyDictionary<string, CamFile> Files { get; private init; }

public CamChunk(DateTime timestamp, IEnumerable<CamFile> files)
Expand All @@ -16,24 +22,16 @@ public CamChunk(DateTime timestamp, IEnumerable<CamFile> files)
}

/// <summary>
/// Finds all the media files in the directory and group them by timestamp into chunks.
/// Groups valid media files by timestamp and keeps chunks with front-camera video.
/// </summary>
public static LinkedList<CamChunk> Map(string directory)
public static IReadOnlyList<CamChunk> Map(string directory)
{
var chunks = CamFile.FindFiles(directory)
return CamFile.FindFiles(directory)
.GroupBy(f => f.Timestamp)
.Where(g => g.Any(x => x.Camera == "front")) // Must have a front camera clip to be a valid chunk.
.Where(g => g.Any(file => file.Camera == CameraNames.Front))
.OrderBy(g => g.Key)
.Select(g => new CamChunk(g.Key, g));

var linkedList = new LinkedList<CamChunk>();

foreach (var chunk in chunks)
{
linkedList.AddLast(chunk);
}

return linkedList;
.Select(g => new CamChunk(g.Key, g))
.ToList();
}

public override string ToString() => $"{Timestamp}";
Expand Down
51 changes: 27 additions & 24 deletions SentryReplay.Data/CamClip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,94 +5,87 @@
namespace SentryReplay;

/// <summary>
/// A folder containing a collection of <see cref="CamChunk"/>s that make up a single continuous dashcam clip.
/// A folder containing chunks that make up one continuous dashcam clip.
/// </summary>
public partial record class CamClip
{
/// <summary>
/// The path to the directory containing all the media files and metadata for this clip.
/// Full path to the clip folder.
/// </summary>
public string FullPath { get; private init; }

/// <summary>
/// The title from the folder name or timestamp.
/// Display name from the folder name or parsed timestamp.
/// </summary>
public string Name { get; private init; }

/// <summary>
/// The timestamp parsed from the folder name title if it's available.
/// Timestamp parsed from the folder name or event metadata when available.
/// </summary>
public DateTime Timestamp { get; private init; }

/// <summary>
/// The ordered list of chunks that make up the clip as a whole.
/// Ordered chunks in this clip.
/// </summary>
public LinkedList<CamChunk> Chunks { get; private init; }
public IReadOnlyList<CamChunk> Chunks { get; private init; }

/// <summary>
/// The event data associated with this clip.
/// Optional event metadata for this clip.
/// </summary>
public CamEvent Event { get; private init; }

/// <summary>
/// The path to the thumbnail image for this clip.
/// Path to the thumbnail image for this clip.
/// </summary>
public string ThumbnailPath { get; private init; }

public CamClip(string path, string name, DateTime timestamp, LinkedList<CamChunk> chunks, CamEvent camEvent)
public CamClip(string path, string name, DateTime timestamp, IEnumerable<CamChunk> chunks, CamEvent camEvent)
{
FullPath = Path.GetFullPath(path);
Name = name;
Timestamp = timestamp;
Chunks = chunks;
Chunks = chunks.ToList();
Event = camEvent;
ThumbnailPath = Path.Combine(FullPath, "thumb.png");
}

/// <summary>
/// Maps a clip folder, or returns null when the folder has no playable chunks.
/// </summary>
public static CamClip Map(string directory)
{
var eventData = CamEvent.FromFile(Path.Combine(directory, "event.json"));
var title = Path.GetFileName(directory);
DateTime timestamp = default;

// If the folder name is in the default format we can parse the date to make it look better; otherwise we use the renamed title.
var match = FolderNameRegex().Match(title);
if (match.Success)
{
timestamp = DateTime.ParseExact(match.Groups["date"].Value, "yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
title = timestamp.ToString(CultureInfo.InvariantCulture);
}
else
else if (eventData?.Timestamp != default)
{
// We can try to get the timestamp from the event data if the folder name is missing it.
if (eventData is not null && eventData.Timestamp != default)
{
timestamp = eventData.Timestamp;
}
timestamp = eventData.Timestamp;
}

var chunks = CamChunk.Map(directory);

if (chunks.Count == 0)
{
// Folder does not contain any valid chunks and serves no purpose.
return null;
}

return new(directory, title, timestamp, chunks, eventData);
}

/// <summary>
/// Finds all the clip folders inside the specified root directory.
/// Finds clip folders inside the specified root directory.
/// </summary>
public static IEnumerable<CamClip> FindClips(string rootDirectory)
{
var directories = Directory.GetDirectories(rootDirectory, "*", SearchOption.AllDirectories);

foreach (var directory in directories)
foreach (var directory in EnumerateClipCandidates(rootDirectory))
{
var clip = Map(directory);

if (clip is not null)
{
yield return clip;
Expand Down Expand Up @@ -120,6 +113,16 @@ public string Summary

public override string ToString() => $"{Name}";

private static IEnumerable<string> EnumerateClipCandidates(string rootDirectory)
{
yield return rootDirectory;

foreach (var directory in Directory.EnumerateDirectories(rootDirectory, "*", SearchOption.AllDirectories))
{
yield return directory;
}
}

[GeneratedRegex(@"(?<date>\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})")]
private static partial Regex FolderNameRegex();
}
21 changes: 14 additions & 7 deletions SentryReplay.Data/CamEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,55 @@
namespace SentryReplay;

/// <summary>
/// The <c>event.json</c> metadata for a <see cref="CamClip"/>.
/// Metadata from a TeslaCam <c>event.json</c> file.
/// </summary>
public record class CamEvent
{
/// <summary>
/// The ISO 8601 timestamp indicating when the event occurred.
/// Event timestamp.
/// </summary>
[JsonPropertyName("timestamp")]
public DateTime Timestamp { get; init; }

/// <summary>
/// The nearest city to the event, as determined by the vehicle's GPS system.
/// Nearest city reported by the vehicle.
/// </summary>
[JsonPropertyName("city")]
public string City { get; init; }

/// <summary>
/// The estimated latitude of the vehicle when the event occurred.
/// Estimated latitude.
/// </summary>
[JsonPropertyName("est_lat")]
public decimal EstLat { get; init; }

/// <summary>
/// The estimated longitude of the vehicle when the event occurred.
/// Estimated longitude.
/// </summary>
[JsonPropertyName("est_lon")]
public decimal EstLon { get; init; }

/// <summary>
/// The reason the event was recorded.
/// Recording reason.
/// </summary>
[JsonPropertyName("reason")]
public string Reason { get; init; }

/// <summary>
/// Indicates which camera the event is associated with (if applicable).
/// Camera id reported by the vehicle.
/// </summary>
[JsonPropertyName("camera")]
public int Camera { get; init; }

private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
NumberHandling = JsonNumberHandling.AllowReadingFromString,
PropertyNameCaseInsensitive = true,
};

/// <summary>
/// Deserializes event JSON and returns null for malformed payloads.
/// </summary>
public static CamEvent Deserialize(string json)
{
try
Expand All @@ -61,6 +65,9 @@ public static CamEvent Deserialize(string json)
}
}

/// <summary>
/// Reads and deserializes event metadata when the file exists.
/// </summary>
public static CamEvent FromFile(string path)
{
if (!File.Exists(path))
Expand Down
38 changes: 22 additions & 16 deletions SentryReplay.Data/CamFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@
namespace SentryReplay;

/// <summary>
/// An actual playable dashcam media file from a specified camera angle.
/// A playable dashcam media file for one camera angle.
/// </summary>
public partial record class CamFile
{
/// <summary>
/// The path to the media file.
/// Full path to the media file.
/// </summary>
public string FullPath { get; private init; }

/// <summary>
/// The timestamp of the media file.
/// Timestamp parsed from the TeslaCam file name.
/// </summary>
public DateTime Timestamp { get; private init; }

/// <summary>
/// The name of the camera that recorded the media file.
/// Camera name parsed from the TeslaCam file name.
/// </summary>
public string Camera { get; private init; }

Expand All @@ -31,25 +31,31 @@ public CamFile(string path, DateTime timestamp, string camera)
}

/// <summary>
/// Finds all the media files in the directory that match the typical format.
/// Finds valid TeslaCam media files in a single directory.
/// </summary>
public static IEnumerable<CamFile> FindFiles(string rootDirectory)
{
var files = Directory.EnumerateFiles(rootDirectory, "*", SearchOption.TopDirectoryOnly);
return Directory.EnumerateFiles(rootDirectory, "*.mp4", SearchOption.TopDirectoryOnly)
.Select(TryMap)
.OfType<CamFile>()
.OrderBy(file => file.Timestamp)
.ThenBy(file => file.Camera);
}

public override string ToString() => $"{Camera}";

foreach (var file in files)
private static CamFile TryMap(string path)
{
var match = FileNameRegex().Match(Path.GetFileName(path));
if (!match.Success)
{
var match = FileNameRegex().Match(Path.GetFileName(file));
if (match.Success)
{
var timestamp = DateTime.ParseExact(match.Groups["date"].Value, "yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
var camera = match.Groups["camera"].Value;
yield return new CamFile(file, timestamp, camera);
}
return null;
}
}

public override string ToString() => $"{Camera}";
var timestamp = DateTime.ParseExact(match.Groups["date"].Value, "yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
var camera = match.Groups["camera"].Value;
return new CamFile(path, timestamp, camera);
}

[GeneratedRegex(@"(?<date>\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})-(?<camera>.+)\.mp4")]
private static partial Regex FileNameRegex();
Expand Down
Loading
Loading