diff --git a/tests/core/test_dash_caption_sdh.py b/tests/core/test_dash_caption_sdh.py
new file mode 100644
index 00000000..ab2ac96c
--- /dev/null
+++ b/tests/core/test_dash_caption_sdh.py
@@ -0,0 +1,19 @@
+import xml.etree.ElementTree as ET
+
+from unshackle.core.manifests.dash import DASH
+
+CAPTION = ''
+SDH = ''
+
+
+def test_caption_role_is_closed_caption() -> None:
+ element = ET.fromstring(f"{CAPTION}")
+ assert DASH.is_closed_caption(element) is True
+ assert DASH.is_sdh(element) is False
+
+
+def test_caption_role_with_sdh_value_is_sdh_only() -> None:
+ # a Subtitle cannot be both CC and SDH, so a set marked both used to fail the whole title
+ element = ET.fromstring(f"{CAPTION}{SDH}")
+ assert DASH.is_closed_caption(element) is False
+ assert DASH.is_sdh(element) is True
diff --git a/unshackle/core/manifests/dash.py b/unshackle/core/manifests/dash.py
index f3efc2f5..8c05fa32 100644
--- a/unshackle/core/manifests/dash.py
+++ b/unshackle/core/manifests/dash.py
@@ -1293,8 +1293,8 @@ class DASH:
@staticmethod
def is_closed_caption(adaptation_set: Element) -> bool:
- """Whether the Adaptation Set is a Closed Caption Subtitle."""
- return any(
+ """Whether the Adaptation Set is a Closed Caption Subtitle. A caption that is also marked SDH counts as SDH."""
+ return not DASH.is_sdh(adaptation_set) and any(
(x.get("schemeIdUri"), x.get("value")) == ("urn:mpeg:dash:role:2011", "caption")
for x in adaptation_set.findall("Role")
)